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)]
13#[serde(untagged)]
14pub enum PlaybookSource {
15 File { playbook: String },
16 Content { content: String },
17}
18
19#[derive(Deserialize)]
20pub struct RunPlaybookParams {
21 #[serde(flatten)]
22 pub source: PlaybookSource,
23 pub r#type: Option<String>,
24 pub to: Option<String>,
25}
26
27#[derive(Serialize)]
28pub struct RunPlaybookResponse {
29 pub session_id: String,
30}
31
32#[derive(Serialize)]
33pub struct PlaybookInfo {
34 name: String,
35 updated: String,
36}
37
38#[derive(Serialize)]
39pub struct RecordInfo {
40 id: String,
41 date: String,
42 duration: String,
43 status: String,
44}
45
46pub async fn list_playbooks() -> impl IntoResponse {
47 let mut playbooks = Vec::new();
48 let path = PathBuf::from("config/playbook");
49
50 if let Ok(entries) = fs::read_dir(path) {
51 for entry in entries.flatten() {
52 if let Ok(metadata) = entry.metadata() {
53 if metadata.is_file() {
54 if let Some(name) = entry.file_name().to_str() {
55 if name.ends_with(".md") {
56 let updated = metadata
57 .modified()
58 .ok()
59 .map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339())
60 .unwrap_or_default();
61
62 playbooks.push(PlaybookInfo {
63 name: name.to_string(),
64 updated,
65 });
66 }
67 }
68 }
69 }
70 }
71 }
72
73 Json(playbooks)
74}
75
76pub async fn get_playbook(Path(name): Path<String>) -> impl IntoResponse {
77 let path = PathBuf::from("config/playbook").join(&name);
78
79 if name.contains("..") || name.contains('/') || name.contains('\\') {
81 return (StatusCode::BAD_REQUEST, "Invalid filename").into_response();
82 }
83
84 match fs::read_to_string(path) {
85 Ok(content) => content.into_response(),
86 Err(_) => (StatusCode::NOT_FOUND, "Playbook not found").into_response(),
87 }
88}
89
90pub async fn save_playbook(Path(name): Path<String>, body: String) -> impl IntoResponse {
91 let path = PathBuf::from("config/playbook").join(&name);
92
93 if name.contains("..") || name.contains('/') || name.contains('\\') {
94 return (StatusCode::BAD_REQUEST, "Invalid filename").into_response();
95 }
96
97 if let Some(parent) = path.parent() {
99 let _ = fs::create_dir_all(parent);
100 }
101
102 match fs::write(path, body) {
103 Ok(_) => StatusCode::OK.into_response(),
104 Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
105 }
106}
107
108pub async fn list_records(State(state): State<AppState>) -> impl IntoResponse {
109 let mut records = Vec::new();
110 let path = PathBuf::from(state.config.recorder_path());
111
112 if let Ok(entries) = fs::read_dir(path) {
113 for entry in entries.flatten() {
114 if let Ok(metadata) = entry.metadata() {
115 if metadata.is_file() {
116 if let Some(name) = entry.file_name().to_str() {
117 if name.ends_with(".jsonl") {
118 let updated = metadata
119 .modified()
120 .ok()
121 .map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339())
122 .unwrap_or_default();
123
124 records.push(RecordInfo {
127 id: name.replace(".events.jsonl", ""),
128 date: updated,
129 duration: "0s".to_string(), status: "completed".to_string(), });
132 }
133 }
134 }
135 }
136 }
137 }
138
139 records.sort_by(|a, b| b.date.cmp(&a.date));
141
142 Json(records)
143}
144
145pub async fn run_playbook(
146 State(state): State<AppState>,
147 Json(params): Json<RunPlaybookParams>,
148) -> impl IntoResponse {
149 let playbook_val = match params.source {
150 PlaybookSource::File { playbook } => playbook,
151 PlaybookSource::Content { content } => content,
152 };
153
154 let session_id = format!("s.{}", Uuid::new_v4().to_string());
155
156 state
158 .pending_playbooks
159 .lock()
160 .await
161 .insert(session_id.clone(), playbook_val);
162
163 Json(RunPlaybookResponse { session_id }).into_response()
166}