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
32        .policy
33        .actions
34        .iter()
35        .map(|a| a.to_iam_action())
36        .collect();
37    let text = format!(
38        "Audex session started\n\u{2022} Session: `{}`\n\u{2022} Role: `{}`\n\u{2022} Actions: `{}`\n\u{2022} Command: `{}`\n\u{2022} TTL: {}s",
39        &session.id[..8],
40        session.role_arn,
41        actions.join(", "),
42        session.command.join(" "),
43        session.ttl_seconds,
44    );
45    send(target, &text).await
46}
47
48/// Send a session end notification.
49pub async fn notify_end(
50    target: &NotifyTarget,
51    session: &Session,
52    exit_code: Option<i32>,
53) -> Result<()> {
54    let emoji = match session.status.to_string().as_str() {
55        "completed" => "\u{2705}",
56        "failed" => "\u{274c}",
57        _ => "\u{26a0}\u{fe0f}",
58    };
59    let text = format!(
60        "{} Audex session ended\n\u{2022} Session: `{}`\n\u{2022} Status: {}\n\u{2022} Command: `{}`{}",
61        emoji,
62        &session.id[..8],
63        session.status,
64        session.command.join(" "),
65        exit_code.map_or(String::new(), |c| format!("\n\u{2022} Exit code: {}", c)),
66    );
67    send(target, &text).await
68}
69
70async fn send(target: &NotifyTarget, text: &str) -> Result<()> {
71    let client = reqwest::Client::new();
72
73    let (url, body) = match target {
74        NotifyTarget::Slack(webhook) => {
75            let url = if webhook.starts_with("https://") {
76                webhook.clone()
77            } else {
78                format!("https://{}", webhook)
79            };
80            (url, serde_json::json!({ "text": text }))
81        }
82        NotifyTarget::Discord(webhook) => {
83            let url = if webhook.starts_with("https://") {
84                webhook.clone()
85            } else {
86                format!("https://{}", webhook)
87            };
88            (url, serde_json::json!({ "content": text }))
89        }
90        NotifyTarget::Generic(url) => (
91            url.clone(),
92            serde_json::json!({
93                "text": text,
94                "source": "audex",
95            }),
96        ),
97    };
98
99    client
100        .post(&url)
101        .header("content-type", "application/json")
102        .json(&body)
103        .send()
104        .await
105        .map_err(|e| AvError::Sts(format!("Webhook notification failed: {}", e)))?;
106
107    Ok(())
108}