active_call/handler/
playbook.rs1use 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 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 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 records.push(RecordInfo {
119 id: name.replace(".events.jsonl", ""),
120 date: updated,
121 duration: "0s".to_string(), status: "completed".to_string(), });
124 }
125 }
126 }
127 }
128 }
129 }
130
131 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 state
145 .pending_playbooks
146 .lock()
147 .await
148 .insert(session_id.clone(), params.playbook.clone());
149
150 Json(RunPlaybookResponse { session_id })
153}