1use gestura_core_foundation::error::AppError;
46use std::collections::HashMap;
47use std::path::PathBuf;
48use std::sync::Arc;
49use tokio::sync::RwLock;
50
51mod runtime;
52
53pub use runtime::{JavaScriptRuntime, LuaRuntime, PythonRuntime};
54
55#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
57pub enum ScriptLanguage {
58 Lua,
59 Python,
60 JavaScript,
61}
62
63#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65pub struct Script {
66 pub id: String,
67 pub name: String,
68 pub description: String,
69 pub language: ScriptLanguage,
70 pub source_code: String,
71 pub entry_point: String,
72 pub author: String,
73 pub version: String,
74 pub permissions: Vec<ScriptPermission>,
75 pub triggers: Vec<ScriptTrigger>,
76 pub is_enabled: bool,
77 pub created_at: chrono::DateTime<chrono::Utc>,
78 pub last_modified: chrono::DateTime<chrono::Utc>,
79 pub execution_count: u32,
80 pub last_error: Option<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
85pub enum ScriptPermission {
86 FileSystem(String), Network(String), SystemCommands,
89 VoiceControl,
90 GestureControl,
91 RingControl,
92 Notifications,
93 ClipboardAccess,
94 WindowManagement,
95 DatabaseAccess,
96}
97
98#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
100pub enum ScriptTrigger {
101 VoiceCommand(String),
102 Gesture(String),
103 TimeSchedule(String), ApplicationEvent(String),
105 FileSystemEvent(String),
106 NetworkEvent(String),
107 UserAction(String),
108 SystemEvent(String),
109}
110
111#[derive(Debug, Clone)]
113pub struct ScriptContext {
114 pub script_id: String,
115 pub user_id: String,
116 pub session_id: String,
117 pub variables: HashMap<String, serde_json::Value>,
118 pub permissions: Vec<ScriptPermission>,
119 pub execution_timeout: std::time::Duration,
120}
121
122#[derive(Debug, Clone, serde::Serialize)]
124pub struct ScriptExecutionResult {
125 pub script_id: String,
126 pub success: bool,
127 pub return_value: Option<serde_json::Value>,
128 pub error_message: Option<String>,
129 pub execution_time_ms: u64,
130 pub output: String,
131 pub warnings: Vec<String>,
132}
133
134#[derive(Debug, Clone)]
136struct ScriptExecution {
137 #[allow(dead_code)]
138 execution_id: String,
139 #[allow(dead_code)]
140 script_id: String,
141 #[allow(dead_code)]
142 started_at: chrono::DateTime<chrono::Utc>,
143 #[allow(dead_code)]
144 context: ScriptContext,
145}
146
147pub struct ScriptingEngine {
152 scripts: Arc<RwLock<HashMap<String, Script>>>,
153 active_executions: Arc<RwLock<HashMap<String, ScriptExecution>>>,
154 lua_runtime: Arc<RwLock<Option<LuaRuntime>>>,
155 python_runtime: Arc<RwLock<Option<PythonRuntime>>>,
156 js_runtime: Arc<RwLock<Option<JavaScriptRuntime>>>,
157 #[allow(dead_code)]
158 script_directory: PathBuf,
159}
160
161impl ScriptingEngine {
162 pub fn new(script_directory: PathBuf) -> Self {
164 Self {
165 scripts: Arc::new(RwLock::new(HashMap::new())),
166 active_executions: Arc::new(RwLock::new(HashMap::new())),
167 lua_runtime: Arc::new(RwLock::new(None)),
168 python_runtime: Arc::new(RwLock::new(None)),
169 js_runtime: Arc::new(RwLock::new(None)),
170 script_directory,
171 }
172 }
173
174 pub async fn initialize(&self) -> Result<(), AppError> {
176 {
178 let mut lua_runtime = self.lua_runtime.write().await;
179 *lua_runtime = Some(LuaRuntime::new()?);
180 }
181
182 {
184 let mut python_runtime = self.python_runtime.write().await;
185 *python_runtime = Some(PythonRuntime::new()?);
186 }
187
188 {
190 let mut js_runtime = self.js_runtime.write().await;
191 *js_runtime = Some(JavaScriptRuntime::new()?);
192 }
193
194 tracing::info!("Scripting engine initialized with Lua, Python, and JavaScript support");
195 Ok(())
196 }
197
198 pub async fn load_script(&self, script_path: &PathBuf) -> Result<String, AppError> {
200 let content = tokio::fs::read_to_string(script_path)
201 .await
202 .map_err(AppError::Io)?;
203
204 let metadata = self.parse_script_metadata(&content, script_path)?;
206 let script_id = metadata.id.clone();
207
208 self.validate_script(&metadata).await?;
210
211 let mut scripts = self.scripts.write().await;
213 scripts.insert(script_id.clone(), metadata);
214
215 tracing::info!("Loaded script: {}", script_id);
216 Ok(script_id)
217 }
218
219 fn parse_script_metadata(
221 &self,
222 content: &str,
223 script_path: &std::path::Path,
224 ) -> Result<Script, AppError> {
225 let language = self.detect_language(script_path)?;
226 let script_id = uuid::Uuid::new_v4().to_string();
227
228 let mut name = script_path
230 .file_stem()
231 .and_then(|s| s.to_str())
232 .unwrap_or("Unnamed Script")
233 .to_string();
234 let mut description = "No description".to_string();
235 let mut author = "Unknown".to_string();
236 let mut version = "1.0.0".to_string();
237 let mut permissions = Vec::new();
238 let mut triggers = Vec::new();
239
240 for line in content.lines() {
241 let line = line.trim();
242 if (line.starts_with("--") || line.starts_with("#") || line.starts_with("//"))
243 && let Some(meta) = line.split_once("@")
244 {
245 let (_, meta_content) = meta;
246 if let Some((key, value)) = meta_content.split_once(" ") {
247 match key {
248 "name" => name = value.to_string(),
249 "description" => description = value.to_string(),
250 "author" => author = value.to_string(),
251 "version" => version = value.to_string(),
252 "permission" => {
253 if let Ok(perm) = self.parse_permission(value) {
254 permissions.push(perm);
255 }
256 }
257 "trigger" => {
258 if let Ok(trigger) = self.parse_trigger(value) {
259 triggers.push(trigger);
260 }
261 }
262 _ => {}
263 }
264 }
265 }
266 }
267
268 Ok(Script {
269 id: script_id,
270 name,
271 description,
272 language,
273 source_code: content.to_string(),
274 entry_point: "main".to_string(),
275 author,
276 version,
277 permissions,
278 triggers,
279 is_enabled: true,
280 created_at: chrono::Utc::now(),
281 last_modified: chrono::Utc::now(),
282 execution_count: 0,
283 last_error: None,
284 })
285 }
286
287 fn detect_language(&self, script_path: &std::path::Path) -> Result<ScriptLanguage, AppError> {
289 match script_path.extension().and_then(|ext| ext.to_str()) {
290 Some("lua") => Ok(ScriptLanguage::Lua),
291 Some("py") => Ok(ScriptLanguage::Python),
292 Some("js") => Ok(ScriptLanguage::JavaScript),
293 _ => Err(AppError::InvalidInput(
294 "Unsupported script language".to_string(),
295 )),
296 }
297 }
298
299 fn parse_permission(&self, perm_str: &str) -> Result<ScriptPermission, AppError> {
301 if let Some(stripped) = perm_str.strip_prefix("filesystem:") {
302 Ok(ScriptPermission::FileSystem(stripped.to_string()))
303 } else if let Some(stripped) = perm_str.strip_prefix("network:") {
304 Ok(ScriptPermission::Network(stripped.to_string()))
305 } else {
306 match perm_str {
307 "system_commands" => Ok(ScriptPermission::SystemCommands),
308 "voice_control" => Ok(ScriptPermission::VoiceControl),
309 "gesture_control" => Ok(ScriptPermission::GestureControl),
310 "ring_control" => Ok(ScriptPermission::RingControl),
311 "notifications" => Ok(ScriptPermission::Notifications),
312 "clipboard" => Ok(ScriptPermission::ClipboardAccess),
313 "window_management" => Ok(ScriptPermission::WindowManagement),
314 "database" => Ok(ScriptPermission::DatabaseAccess),
315 _ => Err(AppError::InvalidInput(format!(
316 "Unknown permission: {}",
317 perm_str
318 ))),
319 }
320 }
321 }
322
323 fn parse_trigger(&self, trigger_str: &str) -> Result<ScriptTrigger, AppError> {
325 if let Some(stripped) = trigger_str.strip_prefix("voice:") {
326 Ok(ScriptTrigger::VoiceCommand(stripped.to_string()))
327 } else if let Some(stripped) = trigger_str.strip_prefix("gesture:") {
328 Ok(ScriptTrigger::Gesture(stripped.to_string()))
329 } else if let Some(stripped) = trigger_str.strip_prefix("schedule:") {
330 Ok(ScriptTrigger::TimeSchedule(stripped.to_string()))
331 } else if let Some(stripped) = trigger_str.strip_prefix("app:") {
332 Ok(ScriptTrigger::ApplicationEvent(stripped.to_string()))
333 } else {
334 Err(AppError::InvalidInput(format!(
335 "Unknown trigger: {}",
336 trigger_str
337 )))
338 }
339 }
340
341 async fn validate_script(&self, script: &Script) -> Result<(), AppError> {
343 for permission in &script.permissions {
345 match permission {
346 ScriptPermission::SystemCommands => {
347 tracing::warn!("Script '{}' requests system command access", script.name);
348 }
349 ScriptPermission::FileSystem(path) if path.contains("..") => {
350 return Err(AppError::PermissionDenied(
351 "Invalid file system permission".to_string(),
352 ));
353 }
354 _ => {}
355 }
356 }
357
358 match script.language {
360 ScriptLanguage::Lua => {
361 if !script.source_code.contains("function") && !script.source_code.contains("=") {
362 tracing::warn!("Lua script may have syntax issues");
363 }
364 }
365 ScriptLanguage::Python => {
366 if script.source_code.contains("import os")
367 && !script
368 .permissions
369 .contains(&ScriptPermission::SystemCommands)
370 {
371 return Err(AppError::PermissionDenied(
372 "Script uses 'os' module without system permission".to_string(),
373 ));
374 }
375 }
376 ScriptLanguage::JavaScript => {
377 if script.source_code.contains("require(")
378 && !script
379 .permissions
380 .contains(&ScriptPermission::SystemCommands)
381 {
382 tracing::warn!("JavaScript script uses require() without system permission");
383 }
384 }
385 }
386
387 Ok(())
388 }
389
390 pub async fn execute_script(
392 &self,
393 script_id: &str,
394 context: ScriptContext,
395 ) -> Result<ScriptExecutionResult, AppError> {
396 let start_time = std::time::Instant::now();
397 let execution_id = uuid::Uuid::new_v4().to_string();
398
399 let scripts = self.scripts.read().await;
401 let script = scripts
402 .get(script_id)
403 .ok_or_else(|| AppError::NotFound(format!("Script not found: {}", script_id)))?
404 .clone();
405
406 if !script.is_enabled {
407 return Err(AppError::PermissionDenied("Script is disabled".to_string()));
408 }
409
410 for required_perm in &script.permissions {
412 if !context.permissions.contains(required_perm) {
413 return Err(AppError::PermissionDenied(format!(
414 "Missing permission: {:?}",
415 required_perm
416 )));
417 }
418 }
419
420 let execution = ScriptExecution {
422 execution_id: execution_id.clone(),
423 script_id: script_id.to_string(),
424 started_at: chrono::Utc::now(),
425 context: context.clone(),
426 };
427
428 {
429 let mut active = self.active_executions.write().await;
430 active.insert(execution_id.clone(), execution);
431 }
432
433 drop(scripts);
434
435 let result = match script.language {
437 ScriptLanguage::Lua => self.execute_lua_script(&script).await,
438 ScriptLanguage::Python => self.execute_python_script(&script).await,
439 ScriptLanguage::JavaScript => self.execute_js_script(&script).await,
440 };
441
442 {
444 let mut active = self.active_executions.write().await;
445 active.remove(&execution_id);
446 }
447
448 {
450 let mut scripts_mut = self.scripts.write().await;
451 if let Some(script_mut) = scripts_mut.get_mut(script_id) {
452 script_mut.execution_count += 1;
453 if let Err(ref error) = result {
454 script_mut.last_error = Some(error.to_string());
455 }
456 }
457 }
458
459 let execution_time = start_time.elapsed().as_millis() as u64;
460
461 match result {
462 Ok((return_value, output, warnings)) => Ok(ScriptExecutionResult {
463 script_id: script_id.to_string(),
464 success: true,
465 return_value,
466 error_message: None,
467 execution_time_ms: execution_time,
468 output,
469 warnings,
470 }),
471 Err(error) => Ok(ScriptExecutionResult {
472 script_id: script_id.to_string(),
473 success: false,
474 return_value: None,
475 error_message: Some(error.to_string()),
476 execution_time_ms: execution_time,
477 output: String::new(),
478 warnings: Vec::new(),
479 }),
480 }
481 }
482
483 async fn execute_lua_script(
485 &self,
486 script: &Script,
487 ) -> Result<(Option<serde_json::Value>, String, Vec<String>), AppError> {
488 let lua_runtime = self.lua_runtime.read().await;
489 if let Some(runtime) = lua_runtime.as_ref() {
490 runtime.execute(&script.source_code).await
491 } else {
492 Err(AppError::NotFound(
493 "Lua runtime not initialized".to_string(),
494 ))
495 }
496 }
497
498 async fn execute_python_script(
500 &self,
501 script: &Script,
502 ) -> Result<(Option<serde_json::Value>, String, Vec<String>), AppError> {
503 let python_runtime = self.python_runtime.read().await;
504 if let Some(runtime) = python_runtime.as_ref() {
505 runtime.execute(&script.source_code).await
506 } else {
507 Err(AppError::NotFound(
508 "Python runtime not initialized".to_string(),
509 ))
510 }
511 }
512
513 async fn execute_js_script(
515 &self,
516 script: &Script,
517 ) -> Result<(Option<serde_json::Value>, String, Vec<String>), AppError> {
518 let js_runtime = self.js_runtime.read().await;
519 if let Some(runtime) = js_runtime.as_ref() {
520 runtime.execute(&script.source_code).await
521 } else {
522 Err(AppError::NotFound(
523 "JavaScript runtime not initialized".to_string(),
524 ))
525 }
526 }
527
528 pub async fn get_scripts(&self) -> Vec<Script> {
530 let scripts = self.scripts.read().await;
531 scripts.values().cloned().collect()
532 }
533
534 pub async fn get_stats(&self) -> serde_json::Value {
536 let scripts = self.scripts.read().await;
537 let active = self.active_executions.read().await;
538
539 let total_scripts = scripts.len();
540 let enabled_scripts = scripts.values().filter(|s| s.is_enabled).count();
541 let active_executions = active.len();
542 let total_executions: u32 = scripts.values().map(|s| s.execution_count).sum();
543
544 serde_json::json!({
545 "total_scripts": total_scripts,
546 "enabled_scripts": enabled_scripts,
547 "active_executions": active_executions,
548 "total_executions": total_executions
549 })
550 }
551}
552
553static SCRIPTING_ENGINE: tokio::sync::OnceCell<ScriptingEngine> =
555 tokio::sync::OnceCell::const_new();
556
557pub async fn get_scripting_engine() -> &'static ScriptingEngine {
559 SCRIPTING_ENGINE
560 .get_or_init(|| async {
561 let script_dir = std::env::current_dir()
562 .unwrap_or_else(|_| PathBuf::from("."))
563 .join("scripts");
564 ScriptingEngine::new(script_dir)
565 })
566 .await
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use tempfile::TempDir;
573
574 #[tokio::test]
575 async fn test_script_loading() {
576 let temp_dir = TempDir::new().unwrap();
577 let engine = ScriptingEngine::new(temp_dir.path().to_path_buf());
578
579 let script_path = temp_dir.path().join("test.lua");
580 let script_content = r#"
581-- @name Test Script
582-- @description A test script
583-- @author Test Author
584-- @version 1.0.0
585-- @permission voice_control
586-- @trigger voice:test
587
588function main()
589 print("Hello from Lua!")
590 return "success"
591end
592"#;
593
594 tokio::fs::write(&script_path, script_content)
595 .await
596 .unwrap();
597
598 let script_id = engine.load_script(&script_path).await.unwrap();
599 let scripts = engine.get_scripts().await;
600
601 assert_eq!(scripts.len(), 1);
602 assert_eq!(scripts[0].id, script_id);
603 assert_eq!(scripts[0].name, "Test Script");
604 }
605
606 #[tokio::test]
607 async fn test_script_execution() {
608 let temp_dir = TempDir::new().unwrap();
609 let engine = ScriptingEngine::new(temp_dir.path().to_path_buf());
610 engine.initialize().await.unwrap();
611
612 let script_path = temp_dir.path().join("test.lua");
613 let script_content = "print('Hello World!')";
614
615 tokio::fs::write(&script_path, script_content)
616 .await
617 .unwrap();
618
619 let script_id = engine.load_script(&script_path).await.unwrap();
620
621 let context = ScriptContext {
622 script_id: script_id.clone(),
623 user_id: "user1".to_string(),
624 session_id: "session1".to_string(),
625 variables: HashMap::new(),
626 permissions: vec![ScriptPermission::VoiceControl],
627 execution_timeout: std::time::Duration::from_secs(30),
628 };
629
630 let result = engine.execute_script(&script_id, context).await.unwrap();
631 assert!(result.success);
632 }
633
634 #[test]
635 fn test_script_language_enum() {
636 assert_eq!(ScriptLanguage::Lua, ScriptLanguage::Lua);
637 assert_ne!(ScriptLanguage::Lua, ScriptLanguage::Python);
638 }
639
640 #[test]
641 fn test_script_permission_enum() {
642 assert_eq!(
643 ScriptPermission::VoiceControl,
644 ScriptPermission::VoiceControl
645 );
646 assert!(matches!(
647 ScriptPermission::FileSystem("/tmp".to_string()),
648 ScriptPermission::FileSystem(_)
649 ));
650 }
651}