1use crate::error::{AvError, Result};
2use crate::session::Session;
3
4pub enum NotifyTarget {
6 Slack(String),
7 Discord(String),
8 Generic(String),
9}
10
11impl NotifyTarget {
12 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
29pub 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
48pub 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}