active_call/handler/
api.rs

1use crate::app::AppState;
2use axum::{
3    extract::{Path, State},
4    http::StatusCode,
5    response::{IntoResponse, Json},
6};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::PathBuf;
10use uuid::Uuid;
11
12#[derive(Deserialize)]
13pub struct RunPlaybookParams {
14    pub playbook: String,
15    pub r#type: Option<String>,
16    pub to: Option<String>,
17}
18
19#[derive(Serialize)]
20pub struct RunPlaybookResponse {
21    pub session_id: String,
22}
23
24#[derive(Serialize)]
25pub struct PlaybookInfo {
26    name: String,
27    updated: String,
28}
29
30#[derive(Serialize)]
31pub struct RecordInfo {
32    id: String,
33    date: String,
34    duration: String,
35    status: String,
36}
37
38pub async fn list_playbooks() -> impl IntoResponse {
39    let mut playbooks = Vec::new();
40    let path = PathBuf::from("config/playbook");
41
42    if let Ok(entries) = fs::read_dir(path) {
43        for entry in entries.flatten() {
44            if let Ok(metadata) = entry.metadata() {
45                if metadata.is_file() {
46                    if let Some(name) = entry.file_name().to_str() {
47                        if name.ends_with(".md") {
48                            let updated = metadata
49                                .modified()
50                                .ok()
51                                .map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339())
52                                .unwrap_or_default();
53
54                            playbooks.push(PlaybookInfo {
55                                name: name.to_string(),
56                                updated,
57                            });
58                        }
59                    }
60                }
61            }
62        }
63    }
64
65    Json(playbooks)
66}
67
68pub async fn get_playbook(Path(name): Path<String>) -> impl IntoResponse {
69    let path = PathBuf::from("config/playbook").join(&name);
70
71    // Security check: prevent directory traversal
72    if name.contains("..") || name.contains('/') || name.contains('\\') {
73        return (StatusCode::BAD_REQUEST, "Invalid filename").into_response();
74    }
75
76    match fs::read_to_string(path) {
77        Ok(content) => content.into_response(),
78        Err(_) => (StatusCode::NOT_FOUND, "Playbook not found").into_response(),
79    }
80}
81
82pub async fn save_playbook(Path(name): Path<String>, body: String) -> impl IntoResponse {
83    let path = PathBuf::from("config/playbook").join(&name);
84
85    if name.contains("..") || name.contains('/') || name.contains('\\') {
86        return (StatusCode::BAD_REQUEST, "Invalid filename").into_response();
87    }
88
89    // Ensure directory exists
90    if let Some(parent) = path.parent() {
91        let _ = fs::create_dir_all(parent);
92    }
93
94    match fs::write(path, body) {
95        Ok(_) => StatusCode::OK.into_response(),
96        Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
97    }
98}
99
100pub async fn list_records(State(state): State<AppState>) -> impl IntoResponse {
101    let mut records = Vec::new();
102    let path = PathBuf::from(state.config.recorder_path());
103
104    if let Ok(entries) = fs::read_dir(path) {
105        for entry in entries.flatten() {
106            if let Ok(metadata) = entry.metadata() {
107                if metadata.is_file() {
108                    if let Some(name) = entry.file_name().to_str() {
109                        if name.ends_with(".jsonl") {
110                            let updated = metadata
111                                .modified()
112                                .ok()
113                                .map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339())
114                                .unwrap_or_default();
115
116                            // In a real app, we'd parse the file to get duration/status
117                            // For now, just return file info
118                            records.push(RecordInfo {
119                                id: name.replace(".events.jsonl", ""),
120                                date: updated,
121                                duration: "0s".to_string(), // Placeholder
122                                status: "completed".to_string(), // Placeholder
123                            });
124                        }
125                    }
126                }
127            }
128        }
129    }
130
131    // Sort by date desc
132    records.sort_by(|a, b| b.date.cmp(&a.date));
133
134    Json(records)
135}
136
137pub async fn run_playbook(
138    State(state): State<AppState>,
139    Json(params): Json<RunPlaybookParams>,
140) -> impl IntoResponse {
141    let session_id = format!("s.{}", Uuid::new_v4().to_string());
142
143    // Store pending playbook
144    state
145        .pending_playbooks
146        .lock()
147        .await
148        .insert(session_id.clone(), params.playbook.clone());
149
150    // TODO: Handle SIP outbound if needed
151
152    Json(RunPlaybookResponse { session_id })
153}
154
155pub async fn index() -> impl IntoResponse {
156    match fs::read_to_string("static/index.html") {
157        Ok(content) => (StatusCode::OK, [("content-type", "text/html")], content).into_response(),
158        Err(_) => (StatusCode::NOT_FOUND, "Index not found").into_response(),
159    }
160}