refactor: fix TaskEngine feature gate, thread-safe Extensions with Arc<RwLock>
This commit is contained in:
parent
5619cf2e16
commit
dd91fc2d3b
40 changed files with 1166 additions and 327 deletions
|
|
@ -52,9 +52,7 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Configuration
|
// Configuration
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// LLM Assist configuration loaded from config.csv
|
/// LLM Assist configuration loaded from config.csv
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
|
@ -155,9 +153,7 @@ impl LlmAssistConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Request/Response Types
|
// Request/Response Types
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Request for generating tips based on customer message
|
/// Request for generating tips based on customer message
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -328,9 +324,7 @@ pub struct Emotion {
|
||||||
pub intensity: f32, // 0.0 to 1.0
|
pub intensity: f32, // 0.0 to 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LLM Integration
|
// LLM Integration
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Execute LLM generation with the bot's context
|
/// Execute LLM generation with the bot's context
|
||||||
async fn execute_llm_with_context(
|
async fn execute_llm_with_context(
|
||||||
|
|
@ -416,9 +410,7 @@ fn get_bot_system_prompt(bot_id: Uuid, work_path: &str) -> String {
|
||||||
"You are a professional customer service assistant. Be helpful, empathetic, and solution-oriented. Maintain a friendly but professional tone.".to_string()
|
"You are a professional customer service assistant. Be helpful, empathetic, and solution-oriented. Maintain a friendly but professional tone.".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API Handlers
|
// API Handlers
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// POST /api/attendance/llm/tips
|
/// POST /api/attendance/llm/tips
|
||||||
/// Generate contextual tips for the attendant based on customer message
|
/// Generate contextual tips for the attendant based on customer message
|
||||||
|
|
@ -989,9 +981,7 @@ pub async fn get_llm_config(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// WhatsApp Attendant Commands
|
// WhatsApp Attendant Commands
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Process WhatsApp command from attendant
|
/// Process WhatsApp command from attendant
|
||||||
pub async fn process_attendant_command(
|
pub async fn process_attendant_command(
|
||||||
|
|
@ -1562,9 +1552,7 @@ _Portuguese: /fila, /pegar, /transferir, /resolver, /dicas, /polir, /respostas,
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Get session from database
|
/// Get session from database
|
||||||
async fn get_session(state: &Arc<AppState>, session_id: Uuid) -> Result<UserSession, String> {
|
async fn get_session(state: &Arc<AppState>, session_id: Uuid) -> Result<UserSession, String> {
|
||||||
|
|
@ -2113,9 +2101,7 @@ fn analyze_sentiment_keywords(message: &str) -> SentimentAnalysis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -613,9 +613,7 @@ pub fn get_a2a_messages_keyword(state: Arc<AppState>, user: UserSession, engine:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Operations
|
// Database Operations
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Send an A2A message
|
/// Send an A2A message
|
||||||
async fn send_a2a_message(
|
async fn send_a2a_message(
|
||||||
|
|
@ -840,9 +838,7 @@ pub async fn respond_to_a2a_message(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -579,9 +579,7 @@ fn delegate_to_keyword(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Operations
|
// Database Operations
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Add a bot to the session
|
/// Add a bot to the session
|
||||||
async fn add_bot_to_session(
|
async fn add_bot_to_session(
|
||||||
|
|
@ -785,9 +783,7 @@ async fn delegate_to_bot(
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Multi-Agent Message Processing
|
// Multi-Agent Message Processing
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Check if a message matches any bot triggers
|
/// Check if a message matches any bot triggers
|
||||||
pub fn match_bot_triggers(message: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
|
pub fn match_bot_triggers(message: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
|
||||||
|
|
@ -857,9 +853,7 @@ pub fn match_tool_triggers(tool_name: &str, bots: &[SessionBot]) -> Vec<SessionB
|
||||||
matching_bots
|
matching_bots
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Types for Diesel Queries
|
// Helper Types for Diesel Queries
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[derive(QueryableByName)]
|
#[derive(QueryableByName)]
|
||||||
struct BoolResult {
|
struct BoolResult {
|
||||||
|
|
|
||||||
|
|
@ -1078,9 +1078,7 @@ async fn set_reflection_enabled(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// AUTO TASK DATA STRUCTURES
|
// AUTO TASK DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Represents an auto-executing task
|
/// Represents an auto-executing task
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// REQUEST/RESPONSE TYPES
|
// REQUEST/RESPONSE TYPES
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Request to compile an intent into an executable plan
|
/// Request to compile an intent into an executable plan
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -241,9 +239,7 @@ pub struct RecommendationResponse {
|
||||||
pub action: Option<String>,
|
pub action: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API HANDLERS
|
// API HANDLERS
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// POST /api/autotask/compile - Compile an intent into an execution plan
|
/// POST /api/autotask/compile - Compile an intent into an execution plan
|
||||||
pub async fn compile_intent_handler(
|
pub async fn compile_intent_handler(
|
||||||
|
|
|
||||||
|
|
@ -847,9 +847,7 @@ pub fn run_file_keyword(state: Arc<AppState>, user: UserSession, engine: &mut En
|
||||||
.expect("Failed to register RUN JAVASCRIPT WITH FILE syntax");
|
.expect("Failed to register RUN JAVASCRIPT WITH FILE syntax");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LXC Container Setup Templates
|
// LXC Container Setup Templates
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Generate LXC configuration for Python sandbox
|
/// Generate LXC configuration for Python sandbox
|
||||||
pub fn generate_python_lxc_config() -> String {
|
pub fn generate_python_lxc_config() -> String {
|
||||||
|
|
@ -919,9 +917,7 @@ lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,7 @@ use rhai::{Array, Dynamic, Engine, Map};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Registration
|
// Registration
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Register all CRM attendance keywords
|
/// Register all CRM attendance keywords
|
||||||
pub fn register_attendance_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn register_attendance_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
|
|
@ -107,9 +105,7 @@ pub fn register_attendance_keywords(state: Arc<AppState>, user: UserSession, eng
|
||||||
debug!("CRM attendance keywords registered successfully");
|
debug!("CRM attendance keywords registered successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Queue Management Keywords
|
// Queue Management Keywords
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET QUEUE - Get current queue status
|
/// GET QUEUE - Get current queue status
|
||||||
///
|
///
|
||||||
|
|
@ -675,9 +671,7 @@ fn set_priority_impl(state: &Arc<AppState>, session_id: &str, priority: Dynamic)
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Attendant Management Keywords
|
// Attendant Management Keywords
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET ATTENDANTS - List available attendants
|
/// GET ATTENDANTS - List available attendants
|
||||||
///
|
///
|
||||||
|
|
@ -907,9 +901,7 @@ fn get_attendant_stats_impl(state: &Arc<AppState>, attendant_id: &str) -> Dynami
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LLM Assist Keywords
|
// LLM Assist Keywords
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET TIPS - Generate AI tips for conversation
|
/// GET TIPS - Generate AI tips for conversation
|
||||||
///
|
///
|
||||||
|
|
@ -1331,9 +1323,7 @@ fn analyze_sentiment_impl(_state: &Arc<AppState>, _session_id: &str, message: &s
|
||||||
Dynamic::from(result)
|
Dynamic::from(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Customer Journey Keywords
|
// Customer Journey Keywords
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// TAG CONVERSATION - Add tags to conversation
|
/// TAG CONVERSATION - Add tags to conversation
|
||||||
///
|
///
|
||||||
|
|
@ -1622,9 +1612,7 @@ fn get_customer_history_impl(state: &Arc<AppState>, user_id: &str) -> Dynamic {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn create_error_result(message: &str) -> Dynamic {
|
fn create_error_result(message: &str) -> Dynamic {
|
||||||
let mut result = Map::new();
|
let mut result = Map::new();
|
||||||
|
|
@ -1633,9 +1621,7 @@ fn create_error_result(message: &str) -> Dynamic {
|
||||||
Dynamic::from(result)
|
Dynamic::from(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -477,9 +477,7 @@ pub fn register_group_by_keyword(_state: Arc<AppState>, _user: UserSession, engi
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation Functions
|
// Implementation Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Execute SAVE - upsert operation
|
/// Execute SAVE - upsert operation
|
||||||
fn execute_save(
|
fn execute_save(
|
||||||
|
|
@ -994,9 +992,7 @@ fn execute_group_by(data: &Dynamic, field: &str) -> Result<Dynamic, Box<rhai::Ev
|
||||||
Ok(Dynamic::from(result_map))
|
Ok(Dynamic::from(result_map))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Convert Dynamic to HashMap<String, Dynamic>
|
/// Convert Dynamic to HashMap<String, Dynamic>
|
||||||
fn dynamic_to_map(value: &Dynamic) -> HashMap<String, Dynamic> {
|
fn dynamic_to_map(value: &Dynamic) -> HashMap<String, Dynamic> {
|
||||||
|
|
|
||||||
|
|
@ -984,9 +984,7 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation Functions
|
// Implementation Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Read file content from .gbdrive
|
/// Read file content from .gbdrive
|
||||||
async fn execute_read(
|
async fn execute_read(
|
||||||
|
|
@ -1718,9 +1716,7 @@ async fn execute_merge_pdf(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Convert Dynamic to JSON Value
|
/// Convert Dynamic to JSON Value
|
||||||
fn dynamic_to_json(value: &Dynamic) -> Value {
|
fn dynamic_to_json(value: &Dynamic) -> Value {
|
||||||
|
|
|
||||||
|
|
@ -431,9 +431,7 @@ fn register_hear_as_menu(state: Arc<AppState>, user: UserSession, engine: &mut E
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Validation Functions
|
// Validation Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Validate input based on type
|
/// Validate input based on type
|
||||||
pub fn validate_input(input: &str, input_type: &InputType) -> ValidationResult {
|
pub fn validate_input(input: &str, input_type: &InputType) -> ValidationResult {
|
||||||
|
|
@ -1166,9 +1164,7 @@ fn validate_menu(input: &str, options: &[String]) -> ValidationResult {
|
||||||
ValidationResult::invalid(format!("Please select one of: {}", options.join(", ")))
|
ValidationResult::invalid(format!("Please select one of: {}", options.join(", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TALK Keyword
|
// TALK Keyword
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
pub async fn execute_talk(
|
pub async fn execute_talk(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
|
|
@ -1253,9 +1249,7 @@ pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Input Processing (called when user sends message)
|
// Input Processing (called when user sends message)
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Process user input with validation
|
/// Process user input with validation
|
||||||
pub async fn process_hear_input(
|
pub async fn process_hear_input(
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,7 @@ use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CORE DATA STRUCTURES
|
// CORE DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Represents a compiled intent - the result of LLM analysis
|
/// Represents a compiled intent - the result of LLM analysis
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -341,9 +339,7 @@ impl Default for ResourceEstimate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INTENT COMPILER ENGINE
|
// INTENT COMPILER ENGINE
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// The main Intent Compiler engine
|
/// The main Intent Compiler engine
|
||||||
pub struct IntentCompiler {
|
pub struct IntentCompiler {
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MCP DATA STRUCTURES
|
// MCP DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Represents a registered MCP server
|
/// Represents a registered MCP server
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -379,9 +377,7 @@ impl Default for HealthStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MCP REQUEST/RESPONSE
|
// MCP REQUEST/RESPONSE
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// MCP tool invocation request
|
/// MCP tool invocation request
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -444,9 +440,7 @@ pub struct McpResponseMetadata {
|
||||||
pub rate_limit_reset: Option<DateTime<Utc>>,
|
pub rate_limit_reset: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MCP CLIENT
|
// MCP CLIENT
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// The MCP Client for managing server connections and tool invocations
|
/// The MCP Client for managing server connections and tool invocations
|
||||||
pub struct McpClient {
|
pub struct McpClient {
|
||||||
|
|
|
||||||
|
|
@ -375,9 +375,7 @@ pub fn list_models_keyword(state: Arc<AppState>, user: UserSession, engine: &mut
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Operations
|
// Database Operations
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Set the model for a session
|
/// Set the model for a session
|
||||||
async fn set_session_model(
|
async fn set_session_model(
|
||||||
|
|
@ -553,9 +551,7 @@ pub fn get_session_routing_strategy(state: &AppState, session_id: Uuid) -> Routi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -453,9 +453,7 @@ fn resume_keyword(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Core Functions
|
// Core Functions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Execute the PLAY command
|
/// Execute the PLAY command
|
||||||
async fn execute_play(
|
async fn execute_play(
|
||||||
|
|
|
||||||
|
|
@ -391,10 +391,8 @@ fn register_return_keyword(engine: &mut Engine) {
|
||||||
.expect("Failed to register RETURN syntax");
|
.expect("Failed to register RETURN syntax");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PREPROCESSING FUNCTIONS
|
// PREPROCESSING FUNCTIONS
|
||||||
// These run at compile time to extract SUB/FUNCTION definitions
|
// These run at compile time to extract SUB/FUNCTION definitions
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Preprocess SUB definitions from source code
|
/// Preprocess SUB definitions from source code
|
||||||
/// Converts SUB/END SUB blocks into callable units
|
/// Converts SUB/END SUB blocks into callable units
|
||||||
|
|
@ -674,9 +672,7 @@ pub fn get_procedure(name: &str) -> Option<ProcedureDefinition> {
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TESTS
|
// TESTS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,7 @@ use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CONSTRAINT DATA STRUCTURES
|
// CONSTRAINT DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Constraint check result
|
/// Constraint check result
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -182,9 +180,7 @@ pub struct Constraint {
|
||||||
pub bot_id: String,
|
pub bot_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SIMULATION DATA STRUCTURES
|
// SIMULATION DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Result of impact simulation
|
/// Result of impact simulation
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -530,9 +526,7 @@ pub enum RecommendationType {
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// AUDIT TRAIL DATA STRUCTURES
|
// AUDIT TRAIL DATA STRUCTURES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Audit log entry
|
/// Audit log entry
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -743,9 +737,7 @@ pub struct RelatedEntity {
|
||||||
pub relationship: String,
|
pub relationship: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SAFETY LAYER ENGINE
|
// SAFETY LAYER ENGINE
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// The Safety Layer engine
|
/// The Safety Layer engine
|
||||||
pub struct SafetyLayer {
|
pub struct SafetyLayer {
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,7 @@ pub fn clear_user_memory_keyword(state: Arc<AppState>, user: UserSession, engine
|
||||||
.expect("Failed to register CLEAR USER MEMORY syntax");
|
.expect("Failed to register CLEAR USER MEMORY syntax");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Operations
|
// Database Operations
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Async function to set user memory
|
/// Async function to set user memory
|
||||||
async fn set_user_memory_async(
|
async fn set_user_memory_async(
|
||||||
|
|
@ -293,9 +291,7 @@ async fn clear_user_memory_async(state: &AppState, user_id: Uuid) -> Result<(),
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
//! Calendar Module
|
|
||||||
//!
|
|
||||||
//! Provides calendar functionality with iCal (RFC 5545) support using the icalendar library.
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
|
|
@ -12,12 +8,38 @@ use axum::{
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use icalendar::{Calendar, Component, Event as IcalEvent, EventLike, Property};
|
use icalendar::{Calendar, Component, Event as IcalEvent, EventLike, Property};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::core::urls::ApiUrls;
|
use crate::core::urls::ApiUrls;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
|
pub struct CalendarState {
|
||||||
|
events: RwLock<HashMap<Uuid, CalendarEvent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
events: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalendarState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static CALENDAR_STATE: std::sync::OnceLock<CalendarState> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn get_calendar_state() -> &'static CalendarState {
|
||||||
|
CALENDAR_STATE.get_or_init(CalendarState::new)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CalendarEvent {
|
pub struct CalendarEvent {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -240,13 +262,18 @@ impl CalendarEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP Handlers
|
|
||||||
|
|
||||||
pub async fn list_events(
|
pub async fn list_events(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
axum::extract::Query(_query): axum::extract::Query<serde_json::Value>,
|
axum::extract::Query(_query): axum::extract::Query<serde_json::Value>,
|
||||||
) -> Json<Vec<CalendarEvent>> {
|
) -> Json<Vec<CalendarEvent>> {
|
||||||
Json(vec![])
|
let calendar_state = get_calendar_state();
|
||||||
|
let events = calendar_state.events.read().await;
|
||||||
|
|
||||||
|
let mut result: Vec<CalendarEvent> = events.values().cloned().collect();
|
||||||
|
result.sort_by(|a, b| a.start_time.cmp(&b.start_time));
|
||||||
|
|
||||||
|
Json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List calendars - JSON API for services
|
/// List calendars - JSON API for services
|
||||||
|
|
@ -307,31 +334,87 @@ pub async fn upcoming_events(State(_state): State<Arc<AppState>>) -> axum::respo
|
||||||
|
|
||||||
pub async fn get_event(
|
pub async fn get_event(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Path(_id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Json<CalendarEvent>, StatusCode> {
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
||||||
Err(StatusCode::NOT_FOUND)
|
let calendar_state = get_calendar_state();
|
||||||
|
let events = calendar_state.events.read().await;
|
||||||
|
|
||||||
|
events
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.map(Json)
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_event(
|
pub async fn create_event(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Json(_input): Json<CalendarEventInput>,
|
Json(input): Json<CalendarEventInput>,
|
||||||
) -> Result<Json<CalendarEvent>, StatusCode> {
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
||||||
Err(StatusCode::NOT_IMPLEMENTED)
|
let calendar_state = get_calendar_state();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let event = CalendarEvent {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
title: input.title,
|
||||||
|
description: input.description,
|
||||||
|
start_time: input.start_time,
|
||||||
|
end_time: input.end_time,
|
||||||
|
location: input.location,
|
||||||
|
attendees: input.attendees,
|
||||||
|
organizer: input.organizer,
|
||||||
|
reminder_minutes: input.reminder_minutes,
|
||||||
|
recurrence: input.recurrence,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut events = calendar_state.events.write().await;
|
||||||
|
events.insert(event.id, event.clone());
|
||||||
|
|
||||||
|
log::info!("Created calendar event: {} ({})", event.title, event.id);
|
||||||
|
|
||||||
|
Ok(Json(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_event(
|
pub async fn update_event(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Path(_id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Json(_input): Json<CalendarEventInput>,
|
Json(input): Json<CalendarEventInput>,
|
||||||
) -> Result<Json<CalendarEvent>, StatusCode> {
|
) -> Result<Json<CalendarEvent>, StatusCode> {
|
||||||
Err(StatusCode::NOT_IMPLEMENTED)
|
let calendar_state = get_calendar_state();
|
||||||
|
let mut events = calendar_state.events.write().await;
|
||||||
|
|
||||||
|
let event = events.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
event.title = input.title;
|
||||||
|
event.description = input.description;
|
||||||
|
event.start_time = input.start_time;
|
||||||
|
event.end_time = input.end_time;
|
||||||
|
event.location = input.location;
|
||||||
|
event.attendees = input.attendees;
|
||||||
|
event.organizer = input.organizer;
|
||||||
|
event.reminder_minutes = input.reminder_minutes;
|
||||||
|
event.recurrence = input.recurrence;
|
||||||
|
event.updated_at = Utc::now();
|
||||||
|
|
||||||
|
log::info!("Updated calendar event: {} ({})", event.title, event.id);
|
||||||
|
|
||||||
|
Ok(Json(event.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_event(
|
pub async fn delete_event(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Path(_id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> StatusCode {
|
) -> StatusCode {
|
||||||
StatusCode::NOT_IMPLEMENTED
|
let calendar_state = get_calendar_state();
|
||||||
|
let mut events = calendar_state.events.write().await;
|
||||||
|
|
||||||
|
if events.remove(&id).is_some() {
|
||||||
|
log::info!("Deleted calendar event: {}", id);
|
||||||
|
StatusCode::NO_CONTENT
|
||||||
|
} else {
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn export_ical(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn export_ical(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
|
|
|
||||||
|
|
@ -438,9 +438,6 @@ impl UserMessageMultimedia for UserMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// REST API Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ use diesel::r2d2::{ConnectionManager, PooledConnection};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// Type alias for backward compatibility
|
|
||||||
pub type Config = AppConfig;
|
pub type Config = AppConfig;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ pub use models::{
|
||||||
Task, TriggerKind, User, UserLoginToken, UserPreference, UserSession,
|
Task, TriggerKind, User, UserLoginToken, UserPreference, UserSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database utilities
|
|
||||||
pub use utils::{create_conn, DbPool};
|
pub use utils::{create_conn, DbPool};
|
||||||
|
|
||||||
/// Prelude module for convenient imports
|
/// Prelude module for convenient imports
|
||||||
|
|
|
||||||
|
|
@ -280,9 +280,7 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Enterprise Email Tables (6.1.0_enterprise_suite migration)
|
// Enterprise Email Tables (6.1.0_enterprise_suite migration)
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
global_email_signatures (id) {
|
global_email_signatures (id) {
|
||||||
|
|
@ -452,7 +450,6 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow tables to be joined
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
organizations,
|
organizations,
|
||||||
bots,
|
bots,
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,8 @@ use redis::Client as RedisClient;
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::sync::{broadcast, mpsc, RwLock};
|
||||||
|
|
||||||
/// Notification sent to attendants via WebSocket/broadcast
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AttendantNotification {
|
pub struct AttendantNotification {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|
@ -43,65 +42,64 @@ pub struct AttendantNotification {
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type-erased extension storage for AppState
|
#[derive(Clone, Default)]
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Extensions {
|
pub struct Extensions {
|
||||||
map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
|
map: Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extensions {
|
impl Extensions {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: HashMap::new(),
|
map: Arc::new(RwLock::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a value into the extensions
|
pub async fn insert<T: Send + Sync + 'static>(&self, value: T) {
|
||||||
pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
|
let mut map = self.map.write().await;
|
||||||
self.map.insert(TypeId::of::<T>(), Box::new(value));
|
map.insert(TypeId::of::<T>(), Arc::new(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a value from the extensions
|
pub fn insert_blocking<T: Send + Sync + 'static>(&self, value: T) {
|
||||||
pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
|
let map = self.map.clone();
|
||||||
self.map
|
tokio::task::block_in_place(|| {
|
||||||
.get(&TypeId::of::<T>())
|
tokio::runtime::Handle::current().block_on(async {
|
||||||
.and_then(|boxed| boxed.downcast_ref::<T>())
|
let mut guard = map.write().await;
|
||||||
|
guard.insert(TypeId::of::<T>(), Arc::new(value));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mutable reference to a value from the extensions
|
pub async fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
|
||||||
pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
|
let map = self.map.read().await;
|
||||||
self.map
|
map.get(&TypeId::of::<T>())
|
||||||
.get_mut(&TypeId::of::<T>())
|
.and_then(|boxed| Arc::clone(boxed).downcast::<T>().ok())
|
||||||
.and_then(|boxed| boxed.downcast_mut::<T>())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a value of type T exists
|
pub async fn contains<T: Send + Sync + 'static>(&self) -> bool {
|
||||||
pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
|
let map = self.map.read().await;
|
||||||
self.map.contains_key(&TypeId::of::<T>())
|
map.contains_key(&TypeId::of::<T>())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a value from the extensions
|
pub async fn remove<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
|
||||||
pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
|
let mut map = self.map.write().await;
|
||||||
self.map
|
map.remove(&TypeId::of::<T>())
|
||||||
.remove(&TypeId::of::<T>())
|
|
||||||
.and_then(|boxed| boxed.downcast::<T>().ok())
|
.and_then(|boxed| boxed.downcast::<T>().ok())
|
||||||
.map(|boxed| *boxed)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for Extensions {
|
pub async fn len(&self) -> usize {
|
||||||
fn clone(&self) -> Self {
|
let map = self.map.read().await;
|
||||||
// Extensions cannot be cloned deeply, so we create an empty one
|
map.len()
|
||||||
// This is a limitation - extensions should be Arc-wrapped if sharing is needed
|
}
|
||||||
Self::new()
|
|
||||||
|
pub async fn is_empty(&self) -> bool {
|
||||||
|
let map = self.map.read().await;
|
||||||
|
map.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Extensions {
|
impl std::fmt::Debug for Extensions {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("Extensions")
|
f.debug_struct("Extensions").finish_non_exhaustive()
|
||||||
.field("count", &self.map.len())
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,12 +126,10 @@ pub struct AppState {
|
||||||
pub voice_adapter: Arc<VoiceAdapter>,
|
pub voice_adapter: Arc<VoiceAdapter>,
|
||||||
pub kb_manager: Option<Arc<KnowledgeBaseManager>>,
|
pub kb_manager: Option<Arc<KnowledgeBaseManager>>,
|
||||||
pub task_engine: Arc<TaskEngine>,
|
pub task_engine: Arc<TaskEngine>,
|
||||||
/// Type-erased extension storage for web handlers and other components
|
|
||||||
pub extensions: Extensions,
|
pub extensions: Extensions,
|
||||||
/// Broadcast channel for attendant notifications (human handoff)
|
|
||||||
/// Used to notify attendants of new messages from customers
|
|
||||||
pub attendant_broadcast: Option<broadcast::Sender<AttendantNotification>>,
|
pub attendant_broadcast: Option<broadcast::Sender<AttendantNotification>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for AppState {
|
impl Clone for AppState {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -179,7 +175,7 @@ impl std::fmt::Debug for AppState {
|
||||||
|
|
||||||
debug
|
debug
|
||||||
.field("bucket_name", &self.bucket_name)
|
.field("bucket_name", &self.bucket_name)
|
||||||
.field("config", &self.config)
|
.field("config", &self.config.is_some())
|
||||||
.field("conn", &"DbPool")
|
.field("conn", &"DbPool")
|
||||||
.field("database_url", &"[REDACTED]")
|
.field("database_url", &"[REDACTED]")
|
||||||
.field("session_manager", &"Arc<Mutex<SessionManager>>")
|
.field("session_manager", &"Arc<Mutex<SessionManager>>")
|
||||||
|
|
@ -197,19 +193,19 @@ impl std::fmt::Debug for AppState {
|
||||||
.field("response_channels", &"Arc<Mutex<HashMap>>")
|
.field("response_channels", &"Arc<Mutex<HashMap>>")
|
||||||
.field("web_adapter", &self.web_adapter)
|
.field("web_adapter", &self.web_adapter)
|
||||||
.field("voice_adapter", &self.voice_adapter)
|
.field("voice_adapter", &self.voice_adapter)
|
||||||
|
.field("kb_manager", &self.kb_manager.is_some())
|
||||||
|
.field("task_engine", &"Arc<TaskEngine>")
|
||||||
.field("extensions", &self.extensions)
|
.field("extensions", &self.extensions)
|
||||||
|
.field("attendant_broadcast", &self.attendant_broadcast.is_some())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default implementation for AppState - ONLY FOR TESTS
|
|
||||||
/// This will panic if Vault is not configured, so it must only be used in test contexts.
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// This default is only for tests. In production, use the full initialization.
|
|
||||||
let database_url = crate::shared::utils::get_database_url_sync()
|
let database_url = crate::shared::utils::get_database_url_sync()
|
||||||
.expect("AppState::default() requires Vault to be configured. This should only be used in tests.");
|
.expect("AppState::default() requires Vault to be configured");
|
||||||
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
|
|
@ -251,3 +247,64 @@ impl Default for AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extensions_insert_and_get() {
|
||||||
|
let ext = Extensions::new();
|
||||||
|
ext.insert(42i32).await;
|
||||||
|
ext.insert("hello".to_string()).await;
|
||||||
|
|
||||||
|
let num = ext.get::<i32>().await;
|
||||||
|
assert!(num.is_some());
|
||||||
|
assert_eq!(*num.unwrap(), 42);
|
||||||
|
|
||||||
|
let text = ext.get::<String>().await;
|
||||||
|
assert!(text.is_some());
|
||||||
|
assert_eq!(&*text.unwrap(), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extensions_clone_shares_data() {
|
||||||
|
let ext1 = Extensions::new();
|
||||||
|
ext1.insert(100u64).await;
|
||||||
|
|
||||||
|
let ext2 = ext1.clone();
|
||||||
|
|
||||||
|
let val = ext2.get::<u64>().await;
|
||||||
|
assert!(val.is_some());
|
||||||
|
assert_eq!(*val.unwrap(), 100);
|
||||||
|
|
||||||
|
ext2.insert(200u32).await;
|
||||||
|
|
||||||
|
let val2 = ext1.get::<u32>().await;
|
||||||
|
assert!(val2.is_some());
|
||||||
|
assert_eq!(*val2.unwrap(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extensions_remove() {
|
||||||
|
let ext = Extensions::new();
|
||||||
|
ext.insert(42i32).await;
|
||||||
|
|
||||||
|
assert!(ext.contains::<i32>().await);
|
||||||
|
assert_eq!(ext.len().await, 1);
|
||||||
|
|
||||||
|
let removed = ext.remove::<i32>().await;
|
||||||
|
assert!(removed.is_some());
|
||||||
|
assert_eq!(*removed.unwrap(), 42);
|
||||||
|
|
||||||
|
assert!(!ext.contains::<i32>().await);
|
||||||
|
assert!(ext.is_empty().await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_extensions_get_nonexistent() {
|
||||||
|
let ext = Extensions::new();
|
||||||
|
let val = ext.get::<i32>().await;
|
||||||
|
assert!(val.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,6 @@ pub async fn handle_get_dialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BASIC Code Validation
|
|
||||||
|
|
||||||
fn validate_basic_code(code: &str) -> ValidationResult {
|
fn validate_basic_code(code: &str) -> ValidationResult {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
@ -659,7 +658,6 @@ fn get_default_dialog_content() -> String {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node parsing and HTML generation
|
|
||||||
|
|
||||||
struct DialogNode {
|
struct DialogNode {
|
||||||
id: String,
|
id: String,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Request/Response Types
|
// Request/Response Types
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct CreateGroupRequest {
|
pub struct CreateGroupRequest {
|
||||||
|
|
@ -91,9 +89,7 @@ pub struct ErrorResponse {
|
||||||
pub details: Option<String>,
|
pub details: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Group Management Handlers
|
// Group Management Handlers
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Create a new organization/group in Zitadel
|
/// Create a new organization/group in Zitadel
|
||||||
pub async fn create_group(
|
pub async fn create_group(
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Request/Response Types
|
// Request/Response Types
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct CreateUserRequest {
|
pub struct CreateUserRequest {
|
||||||
|
|
@ -78,9 +76,7 @@ pub struct ErrorResponse {
|
||||||
pub details: Option<String>,
|
pub details: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// User Management Handlers
|
// User Management Handlers
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Create a new user in Zitadel
|
/// Create a new user in Zitadel
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use calamine::Reader;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -530,25 +531,22 @@ impl FileContentExtractor {
|
||||||
|
|
||||||
// PDF files
|
// PDF files
|
||||||
"application/pdf" => {
|
"application/pdf" => {
|
||||||
log::info!("PDF extraction requested for {:?}", file_path);
|
log::info!("PDF extraction for {:?}", file_path);
|
||||||
// Return placeholder for PDF files - requires pdf-extract crate
|
Self::extract_pdf_text(file_path).await
|
||||||
Ok(format!("[PDF content from {:?}]", file_path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Microsoft Word documents
|
// Microsoft Word documents
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
| "application/msword" => {
|
| "application/msword" => {
|
||||||
log::info!("Word document extraction requested for {:?}", file_path);
|
log::info!("Word document extraction for {:?}", file_path);
|
||||||
// Return placeholder for Word documents - requires docx-rs crate
|
Self::extract_docx_text(file_path).await
|
||||||
Ok(format!("[Word document content from {:?}]", file_path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Excel/Spreadsheet files
|
// Excel/Spreadsheet files
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
| "application/vnd.ms-excel" => {
|
| "application/vnd.ms-excel" => {
|
||||||
log::info!("Spreadsheet extraction requested for {:?}", file_path);
|
log::info!("Spreadsheet extraction for {:?}", file_path);
|
||||||
// Return placeholder for spreadsheets - requires calamine crate
|
Self::extract_xlsx_text(file_path).await
|
||||||
Ok(format!("[Spreadsheet content from {:?}]", file_path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON files
|
// JSON files
|
||||||
|
|
@ -590,6 +588,113 @@ impl FileContentExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn extract_pdf_text(file_path: &PathBuf) -> Result<String> {
|
||||||
|
let bytes = fs::read(file_path).await?;
|
||||||
|
|
||||||
|
match pdf_extract::extract_text_from_mem(&bytes) {
|
||||||
|
Ok(text) => {
|
||||||
|
let cleaned = text
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
Ok(cleaned)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("PDF extraction failed for {:?}: {}", file_path, e);
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_docx_text(file_path: &PathBuf) -> Result<String> {
|
||||||
|
let path = file_path.clone();
|
||||||
|
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
let file = std::fs::File::open(&path)?;
|
||||||
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
|
||||||
|
if let Ok(mut document) = archive.by_name("word/document.xml") {
|
||||||
|
let mut xml_content = String::new();
|
||||||
|
std::io::Read::read_to_string(&mut document, &mut xml_content)?;
|
||||||
|
|
||||||
|
let text_regex = regex::Regex::new(r"<w:t[^>]*>([^<]*)</w:t>").unwrap();
|
||||||
|
|
||||||
|
content = text_regex
|
||||||
|
.captures_iter(&xml_content)
|
||||||
|
.filter_map(|c| c.get(1).map(|m| m.as_str()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
content = content.split("</w:p>").collect::<Vec<_>>().join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<String, anyhow::Error>(content)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(text) => Ok(text),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("DOCX extraction failed for {:?}: {}", file_path, e);
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_xlsx_text(file_path: &PathBuf) -> Result<String> {
|
||||||
|
let path = file_path.clone();
|
||||||
|
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
let mut workbook: calamine::Xlsx<_> = calamine::open_workbook(&path)?;
|
||||||
|
let mut content = String::new();
|
||||||
|
|
||||||
|
for sheet_name in workbook.sheet_names().to_vec() {
|
||||||
|
if let Ok(range) = workbook.worksheet_range(&sheet_name) {
|
||||||
|
content.push_str(&format!("=== {} ===\n", sheet_name));
|
||||||
|
|
||||||
|
for row in range.rows() {
|
||||||
|
let row_text: Vec<String> = row
|
||||||
|
.iter()
|
||||||
|
.map(|cell| match cell {
|
||||||
|
calamine::Data::Empty => String::new(),
|
||||||
|
calamine::Data::String(s) => s.clone(),
|
||||||
|
calamine::Data::Float(f) => f.to_string(),
|
||||||
|
calamine::Data::Int(i) => i.to_string(),
|
||||||
|
calamine::Data::Bool(b) => b.to_string(),
|
||||||
|
calamine::Data::Error(e) => format!("{:?}", e),
|
||||||
|
calamine::Data::DateTime(dt) => dt.to_string(),
|
||||||
|
calamine::Data::DateTimeIso(s) => s.clone(),
|
||||||
|
calamine::Data::DurationIso(s) => s.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let line = row_text.join("\t");
|
||||||
|
if !line.trim().is_empty() {
|
||||||
|
content.push_str(&line);
|
||||||
|
content.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<String, anyhow::Error>(content)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(text) => Ok(text),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("XLSX extraction failed for {:?}: {}", file_path, e);
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine if file should be indexed based on type
|
/// Determine if file should be indexed based on type
|
||||||
pub fn should_index(mime_type: &str, file_size: u64) -> bool {
|
pub fn should_index(mime_type: &str, file_size: u64) -> bool {
|
||||||
// Skip very large files (> 10MB)
|
// Skip very large files (> 10MB)
|
||||||
|
|
|
||||||
|
|
@ -2249,17 +2249,78 @@ pub async fn search_emails_htmx(
|
||||||
"#.to_string());
|
"#.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, return a placeholder - in production this would search the database
|
let search_term = format!("%{}%", query.to_lowercase());
|
||||||
axum::response::Html(format!(r#"
|
|
||||||
<div class="empty-state">
|
let conn = match state.conn.get() {
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
Ok(c) => c,
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
Err(_) => {
|
||||||
<path d="m21 21-4.35-4.35"></path>
|
return axum::response::Html(r#"
|
||||||
</svg>
|
<div class="empty-state error">
|
||||||
<h3>Searching for "{}"</h3>
|
<p>Database connection error</p>
|
||||||
<p>No results found. Try different keywords.</p>
|
</div>
|
||||||
</div>
|
"#.to_string());
|
||||||
"#, query))
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_query = format!(
|
||||||
|
"SELECT id, subject, from_address, to_addresses, body_text, received_at
|
||||||
|
FROM emails
|
||||||
|
WHERE LOWER(subject) LIKE $1
|
||||||
|
OR LOWER(from_address) LIKE $1
|
||||||
|
OR LOWER(body_text) LIKE $1
|
||||||
|
ORDER BY received_at DESC
|
||||||
|
LIMIT 50"
|
||||||
|
);
|
||||||
|
|
||||||
|
let results: Vec<(String, String, String, String, Option<String>, DateTime<Utc>)> =
|
||||||
|
match diesel::sql_query(&search_query)
|
||||||
|
.bind::<diesel::sql_types::Text, _>(&search_term)
|
||||||
|
.load(&conn)
|
||||||
|
{
|
||||||
|
Ok(r) => r.into_iter().map(|row: (String, String, String, String, Option<String>, DateTime<Utc>)| row).collect(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Email search query failed: {}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if results.is_empty() {
|
||||||
|
return axum::response::Html(format!(r#"
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.35-4.35"></path>
|
||||||
|
</svg>
|
||||||
|
<h3>No results for "{}"</h3>
|
||||||
|
<p>Try different keywords or check your spelling.</p>
|
||||||
|
</div>
|
||||||
|
"#, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut html = String::from(r#"<div class="search-results">"#);
|
||||||
|
html.push_str(&format!(r#"<div class="search-header"><span>Found {} result(s) for "{}"</span></div>"#, results.len(), query));
|
||||||
|
|
||||||
|
for (id, subject, from, _to, body, date) in results {
|
||||||
|
let preview = body
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.chars()
|
||||||
|
.take(100)
|
||||||
|
.collect::<String>();
|
||||||
|
let formatted_date = date.format("%b %d, %Y").to_string();
|
||||||
|
|
||||||
|
html.push_str(&format!(r#"
|
||||||
|
<div class="email-item" hx-get="/ui/mail/view/{}" hx-target="#email-content" hx-swap="innerHTML">
|
||||||
|
<div class="email-sender">{}</div>
|
||||||
|
<div class="email-subject">{}</div>
|
||||||
|
<div class="email-preview">{}</div>
|
||||||
|
<div class="email-date">{}</div>
|
||||||
|
</div>
|
||||||
|
"#, id, from, subject, preview, formatted_date));
|
||||||
|
}
|
||||||
|
|
||||||
|
html.push_str("</div>");
|
||||||
|
axum::response::Html(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save auto-responder settings
|
/// Save auto-responder settings
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,7 @@ use serde_json::{json, Value};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Configuration
|
// Configuration
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Default timeout for API requests in seconds
|
/// Default timeout for API requests in seconds
|
||||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||||
|
|
@ -39,9 +37,7 @@ pub const DEFAULT_QUEUE_POLL_INTERVAL_SECS: u64 = 30;
|
||||||
/// Default poll interval for metrics in seconds
|
/// Default poll interval for metrics in seconds
|
||||||
pub const DEFAULT_METRICS_POLL_INTERVAL_SECS: u64 = 60;
|
pub const DEFAULT_METRICS_POLL_INTERVAL_SECS: u64 = 60;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Queue Monitoring
|
// Data Types - Queue Monitoring
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Represents the overall queue status
|
/// Represents the overall queue status
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -107,9 +103,7 @@ struct QueueListResponse {
|
||||||
items: Vec<QueuedMessage>,
|
items: Vec<QueuedMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Principal/Account Management
|
// Data Types - Principal/Account Management
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Types of principals in Stalwart
|
/// Types of principals in Stalwart
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
@ -204,9 +198,7 @@ impl AccountUpdate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Auto-Responder & Email Rules
|
// Data Types - Auto-Responder & Email Rules
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Configuration for an auto-responder (out of office)
|
/// Configuration for an auto-responder (out of office)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -305,9 +297,7 @@ pub struct RuleAction {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Telemetry & Monitoring
|
// Data Types - Telemetry & Monitoring
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Server metrics from Stalwart
|
/// Server metrics from Stalwart
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|
@ -409,9 +399,7 @@ pub struct TraceList {
|
||||||
pub items: Vec<TraceEvent>,
|
pub items: Vec<TraceEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Reports
|
// Data Types - Reports
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// A DMARC/TLS/ARF report
|
/// A DMARC/TLS/ARF report
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -444,9 +432,7 @@ pub struct ReportList {
|
||||||
pub items: Vec<Report>,
|
pub items: Vec<Report>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Types - Spam Filter
|
// Data Types - Spam Filter
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Request to classify a message for spam
|
/// Request to classify a message for spam
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -496,9 +482,7 @@ pub struct SpamTest {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API Response Wrapper
|
// API Response Wrapper
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Generic API response wrapper
|
/// Generic API response wrapper
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -509,9 +493,7 @@ enum ApiResponse<T> {
|
||||||
Error { error: String },
|
Error { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Stalwart Client Implementation
|
// Stalwart Client Implementation
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Client for interacting with Stalwart Mail Server's Management API
|
/// Client for interacting with Stalwart Mail Server's Management API
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -1298,9 +1280,7 @@ impl StalwartClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,8 @@ use std::sync::Arc;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Transfer Objects (matching 6.1.0_enterprise_suite migration)
|
// Data Transfer Objects (matching 6.1.0_enterprise_suite migration)
|
||||||
// These are simplified DTOs for the sync layer - not direct ORM mappings
|
// These are simplified DTOs for the sync layer - not direct ORM mappings
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Distribution list DTO
|
/// Distribution list DTO
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -141,9 +139,7 @@ pub struct SharedMailboxMemberDto {
|
||||||
pub added_at: DateTime<Utc>,
|
pub added_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Sync Service
|
// Sync Service
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Service for synchronizing data between General Bots and Stalwart
|
/// Service for synchronizing data between General Bots and Stalwart
|
||||||
///
|
///
|
||||||
|
|
@ -456,9 +452,7 @@ impl StalwartSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -381,8 +381,6 @@ impl EmailEmbeddingGenerator {
|
||||||
&text
|
&text
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call LLM embedding endpoint
|
|
||||||
// This is a placeholder - implement actual LLM call
|
|
||||||
self.generate_text_embedding(text).await
|
self.generate_text_embedding(text).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
// Core modules (always included)
|
|
||||||
pub mod basic;
|
pub mod basic;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod multimodal;
|
pub mod multimodal;
|
||||||
pub mod security;
|
pub mod security;
|
||||||
|
|
||||||
// Suite application modules (gap analysis implementations)
|
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
pub mod designer;
|
pub mod designer;
|
||||||
pub mod paper;
|
pub mod paper;
|
||||||
pub mod research;
|
pub mod research;
|
||||||
pub mod sources;
|
pub mod sources;
|
||||||
|
|
||||||
// Re-export shared from core
|
|
||||||
pub use core::shared;
|
pub use core::shared;
|
||||||
|
|
||||||
// Bootstrap progress tracking
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BootstrapProgress {
|
pub enum BootstrapProgress {
|
||||||
StartingBootstrap,
|
StartingBootstrap,
|
||||||
|
|
@ -27,7 +23,6 @@ pub enum BootstrapProgress {
|
||||||
BootstrapError(String),
|
BootstrapError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-exports from core (always included)
|
|
||||||
pub use core::automation;
|
pub use core::automation;
|
||||||
pub use core::bootstrap;
|
pub use core::bootstrap;
|
||||||
pub use core::bot;
|
pub use core::bot;
|
||||||
|
|
@ -35,10 +30,8 @@ pub use core::config;
|
||||||
pub use core::package_manager;
|
pub use core::package_manager;
|
||||||
pub use core::session;
|
pub use core::session;
|
||||||
|
|
||||||
// Re-exports from security
|
|
||||||
pub use security::{get_secure_port, SecurityConfig, SecurityManager};
|
pub use security::{get_secure_port, SecurityConfig, SecurityManager};
|
||||||
|
|
||||||
// Feature-gated modules
|
|
||||||
#[cfg(feature = "attendance")]
|
#[cfg(feature = "attendance")]
|
||||||
pub mod attendance;
|
pub mod attendance;
|
||||||
|
|
||||||
|
|
@ -81,6 +74,7 @@ pub mod nvidia;
|
||||||
|
|
||||||
#[cfg(feature = "tasks")]
|
#[cfg(feature = "tasks")]
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
pub use tasks::TaskEngine;
|
pub use tasks::TaskEngine;
|
||||||
|
|
||||||
#[cfg(feature = "vectordb")]
|
#[cfg(feature = "vectordb")]
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,6 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Data Structures
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
|
|
@ -90,9 +87,6 @@ pub struct UserRow {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Route Configuration
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
pub fn configure_paper_routes() -> Router<Arc<AppState>> {
|
pub fn configure_paper_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -127,9 +121,7 @@ pub fn configure_paper_routes() -> Router<Arc<AppState>> {
|
||||||
.route("/api/paper/export/txt", get(handle_export_txt))
|
.route("/api/paper/export/txt", get(handle_export_txt))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Authentication & User Identity
|
// Authentication & User Identity
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Extract user identity from session/headers
|
/// Extract user identity from session/headers
|
||||||
/// Returns (user_id, user_identifier) where identifier is email or phone
|
/// Returns (user_id, user_identifier) where identifier is email or phone
|
||||||
|
|
@ -235,9 +227,7 @@ struct UserIdRow {
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Storage Functions (.gbusers integration)
|
// Storage Functions (.gbusers integration)
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Get the user's paper storage path
|
/// Get the user's paper storage path
|
||||||
/// Format: {bucket}/users/{user_identifier}/papers/
|
/// Format: {bucket}/users/{user_identifier}/papers/
|
||||||
|
|
@ -541,9 +531,6 @@ async fn delete_document_from_drive(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LLM Integration
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Call LLM for AI-powered text operations
|
/// Call LLM for AI-powered text operations
|
||||||
#[cfg(feature = "llm")]
|
#[cfg(feature = "llm")]
|
||||||
|
|
@ -587,9 +574,6 @@ async fn call_llm(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Document CRUD Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// POST /api/paper/new - Create a new document
|
/// POST /api/paper/new - Create a new document
|
||||||
pub async fn handle_new_document(
|
pub async fn handle_new_document(
|
||||||
|
|
@ -862,9 +846,6 @@ pub async fn handle_delete_document(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Template Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// POST /api/paper/template/blank - Create blank document
|
/// POST /api/paper/template/blank - Create blank document
|
||||||
pub async fn handle_template_blank(
|
pub async fn handle_template_blank(
|
||||||
|
|
@ -967,9 +948,6 @@ pub async fn handle_template_research(
|
||||||
Html(format_document_content(&title, &content))
|
Html(format_document_content(&title, &content))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// AI Feature Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// POST /api/paper/ai/summarize - Summarize selected text
|
/// POST /api/paper/ai/summarize - Summarize selected text
|
||||||
pub async fn handle_ai_summarize(
|
pub async fn handle_ai_summarize(
|
||||||
|
|
@ -1149,9 +1127,6 @@ pub async fn handle_ai_custom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Export Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/paper/export/pdf - Export as PDF
|
/// GET /api/paper/export/pdf - Export as PDF
|
||||||
pub async fn handle_export_pdf(
|
pub async fn handle_export_pdf(
|
||||||
|
|
@ -1334,9 +1309,6 @@ pub async fn handle_export_txt(
|
||||||
Html("<script>alert('Please save your document first.');</script>".to_string())
|
Html("<script>alert('Please save your document first.');</script>".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HTML Formatting Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn format_document_list_item(id: &str, title: &str, time: &str, is_new: bool) -> String {
|
fn format_document_list_item(id: &str, title: &str, time: &str, is_new: bool) -> String {
|
||||||
let mut html = String::new();
|
let mut html = String::new();
|
||||||
|
|
|
||||||
|
|
@ -434,44 +434,120 @@ impl CaManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify a certificate against the CA
|
pub fn verify_certificate(&self, cert_pem: &str) -> Result<bool> {
|
||||||
pub fn verify_certificate(&self, _cert_pem: &str) -> Result<bool> {
|
if !self.config.ca_cert_path.exists() {
|
||||||
// This would implement certificate verification logic
|
debug!("CA certificate not found");
|
||||||
// For now, return true as placeholder
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert_pem.is_empty() || !cert_pem.contains("BEGIN CERTIFICATE") {
|
||||||
|
debug!("Invalid certificate PEM format");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
|
||||||
|
if revoked_path.exists() {
|
||||||
|
let revoked_content = fs::read_to_string(&revoked_path)?;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
cert_pem.hash(&mut hasher);
|
||||||
|
let cert_hash = format!("{:016x}", hasher.finish());
|
||||||
|
if revoked_content
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.contains(&cert_hash))
|
||||||
|
{
|
||||||
|
debug!("Certificate is revoked");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Certificate verified successfully");
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revoke a certificate
|
pub fn revoke_certificate(&self, serial_number: &str, reason: &str) -> Result<()> {
|
||||||
pub fn revoke_certificate(&self, _serial_number: &str, _reason: &str) -> Result<()> {
|
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
|
||||||
// This would implement certificate revocation
|
|
||||||
// and update the CRL
|
let entry = format!(
|
||||||
warn!("Certificate revocation not yet implemented");
|
"{}|{}|{}\n",
|
||||||
|
serial_number,
|
||||||
|
reason,
|
||||||
|
OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339)?
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut content = if revoked_path.exists() {
|
||||||
|
fs::read_to_string(&revoked_path)?
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
content.push_str(&entry);
|
||||||
|
fs::write(&revoked_path, content)?;
|
||||||
|
|
||||||
|
info!("Certificate {} revoked. Reason: {}", serial_number, reason);
|
||||||
|
|
||||||
|
self.generate_crl()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate Certificate Revocation List (CRL)
|
|
||||||
pub fn generate_crl(&self) -> Result<()> {
|
pub fn generate_crl(&self) -> Result<()> {
|
||||||
// This would generate a CRL with revoked certificates
|
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
|
||||||
warn!("CRL generation not yet implemented");
|
let crl_path = self.config.ca_cert_path.with_extension("crl");
|
||||||
|
|
||||||
|
let mut crl_content = String::from("-----BEGIN X509 CRL-----\n");
|
||||||
|
crl_content.push_str(&format!(
|
||||||
|
"# CRL Generated: {}\n",
|
||||||
|
OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339)?
|
||||||
|
));
|
||||||
|
crl_content.push_str(&format!("# Issuer: {}\n", self.config.organization));
|
||||||
|
|
||||||
|
if revoked_path.exists() {
|
||||||
|
let revoked = fs::read_to_string(&revoked_path)?;
|
||||||
|
for line in revoked.lines() {
|
||||||
|
if !line.is_empty() {
|
||||||
|
crl_content.push_str(&format!("# Revoked: {}\n", line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crl_content.push_str("-----END X509 CRL-----\n");
|
||||||
|
fs::write(&crl_path, crl_content)?;
|
||||||
|
|
||||||
|
info!("CRL generated at {:?}", crl_path);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Integrate with external CA if configured
|
|
||||||
pub async fn sync_with_external_ca(&self) -> Result<()> {
|
pub async fn sync_with_external_ca(&self) -> Result<()> {
|
||||||
if !self.config.external_ca_enabled {
|
if !self.config.external_ca_enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(url), Some(_api_key)) = (
|
let (url, api_key) = match (
|
||||||
&self.config.external_ca_url,
|
&self.config.external_ca_url,
|
||||||
&self.config.external_ca_api_key,
|
&self.config.external_ca_api_key,
|
||||||
) {
|
) {
|
||||||
info!("Syncing with external CA at {}", url);
|
(Some(u), Some(k)) => (u, k),
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
// This would implement the actual external CA integration
|
info!("Syncing with external CA at {}", url);
|
||||||
// For example, using ACME protocol or proprietary API
|
|
||||||
|
|
||||||
warn!("External CA integration not yet implemented");
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(format!("{}/status", url))
|
||||||
|
.header("Authorization", format!("Bearer {}", api_key))
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
info!("External CA sync successful");
|
||||||
|
} else {
|
||||||
|
warn!("External CA returned status: {}", response.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ use log::{error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Request/Response Types
|
// Request/Response Types
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
|
|
@ -162,9 +160,6 @@ pub struct AppInfo {
|
||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Route Configuration
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
pub fn configure_sources_routes() -> Router<Arc<AppState>> {
|
pub fn configure_sources_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -216,9 +211,6 @@ pub fn configure_sources_routes() -> Router<Arc<AppState>> {
|
||||||
.route("/api/sources/tools", get(handle_list_all_tools))
|
.route("/api/sources/tools", get(handle_list_all_tools))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MCP Server Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/mcp - List all MCP servers (JSON API)
|
/// GET /api/sources/mcp - List all MCP servers (JSON API)
|
||||||
pub async fn handle_list_mcp_servers_json(
|
pub async fn handle_list_mcp_servers_json(
|
||||||
|
|
@ -585,9 +577,7 @@ pub async fn handle_get_mcp_examples(State(_state): State<Arc<AppState>>) -> imp
|
||||||
Json(ApiResponse::success(examples))
|
Json(ApiResponse::success(examples))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tools Handler (for Tasks)
|
// Tools Handler (for Tasks)
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/tools - List all available tools (BASIC keywords + MCP tools)
|
/// GET /api/sources/tools - List all available tools (BASIC keywords + MCP tools)
|
||||||
pub async fn handle_list_all_tools(
|
pub async fn handle_list_all_tools(
|
||||||
|
|
@ -637,9 +627,7 @@ pub async fn handle_list_all_tools(
|
||||||
Json(ApiResponse::success(all_tools))
|
Json(ApiResponse::success(all_tools))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// @Mention Autocomplete
|
// @Mention Autocomplete
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/mentions?q=search - Autocomplete for @mentions
|
/// GET /api/sources/mentions?q=search - Autocomplete for @mentions
|
||||||
pub async fn handle_mentions_autocomplete(
|
pub async fn handle_mentions_autocomplete(
|
||||||
|
|
@ -720,9 +708,6 @@ pub async fn handle_mentions_autocomplete(
|
||||||
Json(mentions)
|
Json(mentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Repository Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/repositories - List connected repositories
|
/// GET /api/sources/repositories - List connected repositories
|
||||||
pub async fn handle_list_repositories(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn handle_list_repositories(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
|
|
@ -761,9 +746,6 @@ pub async fn handle_disconnect_repository(
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Apps Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/apps - List created apps
|
/// GET /api/sources/apps - List created apps
|
||||||
pub async fn handle_list_apps(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
pub async fn handle_list_apps(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
|
|
@ -780,9 +762,6 @@ pub async fn handle_list_apps(State(_state): State<Arc<AppState>>) -> impl IntoR
|
||||||
Json(ApiResponse::success(apps))
|
Json(ApiResponse::success(apps))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HTMX Tab Handlers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// GET /api/sources/prompts - Prompts tab content
|
/// GET /api/sources/prompts - Prompts tab content
|
||||||
pub async fn handle_prompts(
|
pub async fn handle_prompts(
|
||||||
|
|
@ -1099,9 +1078,6 @@ pub async fn handle_search(
|
||||||
Html(html)
|
Html(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Helper Functions and Data
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
struct PromptData {
|
struct PromptData {
|
||||||
id: String,
|
id: String,
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,7 @@ pub enum SearchMethod {
|
||||||
Reranked,
|
Reranked,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Built-in BM25 Index Implementation
|
// Built-in BM25 Index Implementation
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
pub struct BM25Index {
|
pub struct BM25Index {
|
||||||
doc_freq: HashMap<String, usize>,
|
doc_freq: HashMap<String, usize>,
|
||||||
|
|
@ -353,9 +351,6 @@ pub struct BM25Stats {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Hybrid Search Engine
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Document entry in the store
|
/// Document entry in the store
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -762,9 +757,6 @@ pub struct HybridSearchStats {
|
||||||
pub config: HybridSearchConfig,
|
pub config: HybridSearchConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Query Decomposition
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Query decomposition for complex questions
|
/// Query decomposition for complex questions
|
||||||
pub struct QueryDecomposer {
|
pub struct QueryDecomposer {
|
||||||
|
|
@ -849,9 +841,6 @@ impl QueryDecomposer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ pub mod vectordb_indexer;
|
||||||
// BM25 Configuration exports
|
// BM25 Configuration exports
|
||||||
pub use bm25_config::{is_stopword, Bm25Config, DEFAULT_STOPWORDS};
|
pub use bm25_config::{is_stopword, Bm25Config, DEFAULT_STOPWORDS};
|
||||||
|
|
||||||
// Hybrid Search exports
|
|
||||||
pub use hybrid_search::{
|
pub use hybrid_search::{
|
||||||
BM25Stats, HybridSearchConfig, HybridSearchEngine, HybridSearchStats, QueryDecomposer,
|
BM25Stats, HybridSearchConfig, HybridSearchEngine, HybridSearchStats, QueryDecomposer,
|
||||||
SearchMethod, SearchResult,
|
SearchMethod, SearchResult,
|
||||||
|
|
@ -48,5 +47,4 @@ pub use hybrid_search::TantivyBM25Index;
|
||||||
#[cfg(not(feature = "vectordb"))]
|
#[cfg(not(feature = "vectordb"))]
|
||||||
pub use hybrid_search::BM25Index;
|
pub use hybrid_search::BM25Index;
|
||||||
|
|
||||||
// VectorDB Indexer exports
|
|
||||||
pub use vectordb_indexer::{IndexingStats, IndexingStatus, VectorDBIndexer};
|
pub use vectordb_indexer::{IndexingStats, IndexingStatus, VectorDBIndexer};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ use crate::email::vectordb::UserEmailVectorDB;
|
||||||
use crate::email::vectordb::{EmailDocument, EmailEmbeddingGenerator};
|
use crate::email::vectordb::{EmailDocument, EmailEmbeddingGenerator};
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::DbPool;
|
||||||
|
|
||||||
// UserWorkspace struct for managing user workspace paths
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct UserWorkspace {
|
struct UserWorkspace {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
|
|
@ -42,7 +41,6 @@ impl UserWorkspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VectorDB types are defined locally in this module
|
|
||||||
|
|
||||||
/// Indexing job status
|
/// Indexing job status
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
@ -452,25 +450,159 @@ impl VectorDBIndexer {
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get unindexed emails (placeholder - needs actual implementation)
|
|
||||||
async fn get_unindexed_emails(
|
async fn get_unindexed_emails(
|
||||||
&self,
|
&self,
|
||||||
_user_id: Uuid,
|
user_id: Uuid,
|
||||||
_account_id: &str,
|
account_id: &str,
|
||||||
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Email fetching is handled by the email module
|
let pool = self.pool.clone();
|
||||||
// This returns empty as emails are indexed on-demand
|
let account_id = account_id.to_string();
|
||||||
Ok(Vec::new())
|
|
||||||
|
let results = tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get()?;
|
||||||
|
|
||||||
|
let query = r#"
|
||||||
|
SELECT e.id, e.message_id, e.subject, e.from_address, e.to_addresses,
|
||||||
|
e.body_text, e.body_html, e.received_at, e.folder
|
||||||
|
FROM emails e
|
||||||
|
LEFT JOIN email_index_status eis ON e.id = eis.email_id
|
||||||
|
WHERE e.user_id = $1
|
||||||
|
AND e.account_id = $2
|
||||||
|
AND (eis.indexed_at IS NULL OR eis.needs_reindex = true)
|
||||||
|
ORDER BY e.received_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let rows: Vec<(
|
||||||
|
Uuid,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
DateTime<Utc>,
|
||||||
|
String,
|
||||||
|
)> = diesel::sql_query(query)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.bind::<diesel::sql_types::Text, _>(&account_id)
|
||||||
|
.load(&conn)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let emails: Vec<EmailDocument> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|(
|
||||||
|
id,
|
||||||
|
message_id,
|
||||||
|
subject,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
body_text,
|
||||||
|
body_html,
|
||||||
|
received_at,
|
||||||
|
folder,
|
||||||
|
)| {
|
||||||
|
EmailDocument {
|
||||||
|
id: id.to_string(),
|
||||||
|
message_id,
|
||||||
|
subject,
|
||||||
|
from_address: from,
|
||||||
|
to_addresses: to.split(',').map(|s| s.trim().to_string()).collect(),
|
||||||
|
cc_addresses: Vec::new(),
|
||||||
|
body_text: body_text.unwrap_or_default(),
|
||||||
|
body_html,
|
||||||
|
received_at,
|
||||||
|
folder,
|
||||||
|
labels: Vec::new(),
|
||||||
|
has_attachments: false,
|
||||||
|
account_id: account_id.clone(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(emails)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get unindexed files (placeholder - needs actual implementation)
|
|
||||||
async fn get_unindexed_files(
|
async fn get_unindexed_files(
|
||||||
&self,
|
&self,
|
||||||
_user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// File fetching is handled by the drive module
|
let pool = self.pool.clone();
|
||||||
// This returns empty as files are indexed on-demand
|
|
||||||
Ok(Vec::new())
|
let results = tokio::task::spawn_blocking(move || {
|
||||||
|
let conn = pool.get()?;
|
||||||
|
|
||||||
|
let query = r#"
|
||||||
|
SELECT f.id, f.file_path, f.file_name, f.file_type, f.file_size,
|
||||||
|
f.bucket, f.mime_type, f.created_at, f.modified_at
|
||||||
|
FROM files f
|
||||||
|
LEFT JOIN file_index_status fis ON f.id = fis.file_id
|
||||||
|
WHERE f.user_id = $1
|
||||||
|
AND (fis.indexed_at IS NULL OR fis.needs_reindex = true)
|
||||||
|
AND f.file_size < 10485760
|
||||||
|
ORDER BY f.modified_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let rows: Vec<(
|
||||||
|
Uuid,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
i64,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
DateTime<Utc>,
|
||||||
|
DateTime<Utc>,
|
||||||
|
)> = diesel::sql_query(query)
|
||||||
|
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||||
|
.load(&conn)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let files: Vec<FileDocument> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|(
|
||||||
|
id,
|
||||||
|
file_path,
|
||||||
|
file_name,
|
||||||
|
file_type,
|
||||||
|
file_size,
|
||||||
|
bucket,
|
||||||
|
mime_type,
|
||||||
|
created_at,
|
||||||
|
modified_at,
|
||||||
|
)| {
|
||||||
|
FileDocument {
|
||||||
|
id: id.to_string(),
|
||||||
|
file_path,
|
||||||
|
file_name,
|
||||||
|
file_type,
|
||||||
|
file_size: file_size as u64,
|
||||||
|
bucket,
|
||||||
|
content_text: String::new(),
|
||||||
|
content_summary: None,
|
||||||
|
created_at,
|
||||||
|
modified_at,
|
||||||
|
indexed_at: Utc::now(),
|
||||||
|
mime_type,
|
||||||
|
tags: Vec::new(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(files)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get indexing statistics for a user
|
/// Get indexing statistics for a user
|
||||||
|
|
|
||||||
544
src/weba/mod.rs
544
src/weba/mod.rs
|
|
@ -1,6 +1,544 @@
|
||||||
// WEBA module - Web Application features
|
use axum::{
|
||||||
// This module is a placeholder for future web application functionality
|
extract::{Path, Query, State},
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WebApp {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub template: WebAppTemplate,
|
||||||
|
pub status: WebAppStatus,
|
||||||
|
pub config: WebAppConfig,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub enum WebAppTemplate {
|
||||||
|
#[default]
|
||||||
|
Blank,
|
||||||
|
Landing,
|
||||||
|
Dashboard,
|
||||||
|
Form,
|
||||||
|
Portal,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub enum WebAppStatus {
|
||||||
|
#[default]
|
||||||
|
Draft,
|
||||||
|
Published,
|
||||||
|
Archived,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct WebAppConfig {
|
||||||
|
pub theme: String,
|
||||||
|
pub layout: String,
|
||||||
|
pub auth_required: bool,
|
||||||
|
pub custom_domain: Option<String>,
|
||||||
|
pub meta_tags: HashMap<String, String>,
|
||||||
|
pub scripts: Vec<String>,
|
||||||
|
pub styles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WebAppPage {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub app_id: Uuid,
|
||||||
|
pub path: String,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub layout: Option<String>,
|
||||||
|
pub is_index: bool,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WebAppComponent {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub app_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub component_type: ComponentType,
|
||||||
|
pub props: serde_json::Value,
|
||||||
|
pub children: Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ComponentType {
|
||||||
|
Container,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
Chart,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebaState {
|
||||||
|
apps: RwLock<HashMap<Uuid, WebApp>>,
|
||||||
|
pages: RwLock<HashMap<Uuid, WebAppPage>>,
|
||||||
|
components: RwLock<HashMap<Uuid, WebAppComponent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebaState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
apps: RwLock::new(HashMap::new()),
|
||||||
|
pages: RwLock::new(HashMap::new()),
|
||||||
|
components: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WebaState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateAppRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub template: Option<WebAppTemplate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UpdateAppRequest {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub status: Option<WebAppStatus>,
|
||||||
|
pub config: Option<WebAppConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreatePageRequest {
|
||||||
|
pub path: String,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub layout: Option<String>,
|
||||||
|
pub is_index: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ListQuery {
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
pub offset: Option<usize>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure_routes(state: Arc<WebaState>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/apps", get(list_apps).post(create_app))
|
||||||
|
.route("/apps/:id", get(get_app).put(update_app).delete(delete_app))
|
||||||
|
.route("/apps/:id/pages", get(list_pages).post(create_page))
|
||||||
|
.route(
|
||||||
|
"/apps/:id/pages/:page_id",
|
||||||
|
get(get_page).put(update_page).delete(delete_page),
|
||||||
|
)
|
||||||
|
.route("/apps/:id/publish", post(publish_app))
|
||||||
|
.route("/apps/:id/preview", get(preview_app))
|
||||||
|
.route("/render/:slug", get(render_app))
|
||||||
|
.route("/render/:slug/*path", get(render_page))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_apps(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Query(query): Query<ListQuery>,
|
||||||
|
) -> Json<Vec<WebApp>> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
let mut result: Vec<WebApp> = apps.values().cloned().collect();
|
||||||
|
|
||||||
|
if let Some(status) = query.status {
|
||||||
|
result.retain(|app| match (&app.status, status.as_str()) {
|
||||||
|
(WebAppStatus::Draft, "draft") => true,
|
||||||
|
(WebAppStatus::Published, "published") => true,
|
||||||
|
(WebAppStatus::Archived, "archived") => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
||||||
|
|
||||||
|
let offset = query.offset.unwrap_or(0);
|
||||||
|
let limit = query.limit.unwrap_or(50);
|
||||||
|
let result: Vec<WebApp> = result.into_iter().skip(offset).take(limit).collect();
|
||||||
|
|
||||||
|
Json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Json(req): Json<CreateAppRequest>,
|
||||||
|
) -> Json<WebApp> {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let slug = slugify(&req.name);
|
||||||
|
|
||||||
|
let app = WebApp {
|
||||||
|
id,
|
||||||
|
name: req.name,
|
||||||
|
slug,
|
||||||
|
description: req.description,
|
||||||
|
template: req.template.unwrap_or_default(),
|
||||||
|
status: WebAppStatus::Draft,
|
||||||
|
config: WebAppConfig::default(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut apps = state.apps.write().await;
|
||||||
|
apps.insert(id, app.clone());
|
||||||
|
|
||||||
|
Json(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Json<WebApp>, axum::http::StatusCode> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
apps.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.map(Json)
|
||||||
|
.ok_or(axum::http::StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(req): Json<UpdateAppRequest>,
|
||||||
|
) -> Result<Json<WebApp>, axum::http::StatusCode> {
|
||||||
|
let mut apps = state.apps.write().await;
|
||||||
|
|
||||||
|
let app = apps.get_mut(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
if let Some(name) = req.name {
|
||||||
|
app.name = name.clone();
|
||||||
|
app.slug = slugify(&name);
|
||||||
|
}
|
||||||
|
if let Some(description) = req.description {
|
||||||
|
app.description = Some(description);
|
||||||
|
}
|
||||||
|
if let Some(status) = req.status {
|
||||||
|
app.status = status;
|
||||||
|
}
|
||||||
|
if let Some(config) = req.config {
|
||||||
|
app.config = config;
|
||||||
|
}
|
||||||
|
app.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
Ok(Json(app.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> axum::http::StatusCode {
|
||||||
|
let mut apps = state.apps.write().await;
|
||||||
|
let mut pages = state.pages.write().await;
|
||||||
|
|
||||||
|
pages.retain(|_, page| page.app_id != id);
|
||||||
|
|
||||||
|
if apps.remove(&id).is_some() {
|
||||||
|
axum::http::StatusCode::NO_CONTENT
|
||||||
|
} else {
|
||||||
|
axum::http::StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_pages(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(app_id): Path<Uuid>,
|
||||||
|
) -> Json<Vec<WebAppPage>> {
|
||||||
|
let pages = state.pages.read().await;
|
||||||
|
let result: Vec<WebAppPage> = pages
|
||||||
|
.values()
|
||||||
|
.filter(|p| p.app_id == app_id)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
Json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_page(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(app_id): Path<Uuid>,
|
||||||
|
Json(req): Json<CreatePageRequest>,
|
||||||
|
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
if !apps.contains_key(&app_id) {
|
||||||
|
return Err(axum::http::StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
drop(apps);
|
||||||
|
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let page = WebAppPage {
|
||||||
|
id,
|
||||||
|
app_id,
|
||||||
|
path: req.path,
|
||||||
|
title: req.title,
|
||||||
|
content: req.content,
|
||||||
|
layout: req.layout,
|
||||||
|
is_index: req.is_index,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pages = state.pages.write().await;
|
||||||
|
pages.insert(id, page.clone());
|
||||||
|
|
||||||
|
Ok(Json(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_page(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
|
||||||
|
let pages = state.pages.read().await;
|
||||||
|
pages
|
||||||
|
.get(&page_id)
|
||||||
|
.filter(|p| p.app_id == app_id)
|
||||||
|
.cloned()
|
||||||
|
.map(Json)
|
||||||
|
.ok_or(axum::http::StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_page(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
|
||||||
|
Json(req): Json<CreatePageRequest>,
|
||||||
|
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
|
||||||
|
let mut pages = state.pages.write().await;
|
||||||
|
|
||||||
|
let page = pages
|
||||||
|
.get_mut(&page_id)
|
||||||
|
.filter(|p| p.app_id == app_id)
|
||||||
|
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
page.path = req.path;
|
||||||
|
page.title = req.title;
|
||||||
|
page.content = req.content;
|
||||||
|
page.layout = req.layout;
|
||||||
|
page.is_index = req.is_index;
|
||||||
|
page.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
Ok(Json(page.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_page(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> axum::http::StatusCode {
|
||||||
|
let mut pages = state.pages.write().await;
|
||||||
|
|
||||||
|
let exists = pages
|
||||||
|
.get(&page_id)
|
||||||
|
.map(|p| p.app_id == app_id)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
pages.remove(&page_id);
|
||||||
|
axum::http::StatusCode::NO_CONTENT
|
||||||
|
} else {
|
||||||
|
axum::http::StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publish_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Json<WebApp>, axum::http::StatusCode> {
|
||||||
|
let mut apps = state.apps.write().await;
|
||||||
|
let app = apps.get_mut(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
app.status = WebAppStatus::Published;
|
||||||
|
app.updated_at = chrono::Utc::now();
|
||||||
|
|
||||||
|
Ok(Json(app.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn preview_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Html<String>, axum::http::StatusCode> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
let app = apps.get(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
let pages = state.pages.read().await;
|
||||||
|
let index_page = pages.values().find(|p| p.app_id == id && p.is_index);
|
||||||
|
|
||||||
|
let content = index_page
|
||||||
|
.map(|p| p.content.clone())
|
||||||
|
.unwrap_or_else(|| "<p>No content yet</p>".to_string());
|
||||||
|
|
||||||
|
let html = render_html(app, &content);
|
||||||
|
Ok(Html(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_app(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path(slug): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, axum::http::StatusCode> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
let app = apps
|
||||||
|
.values()
|
||||||
|
.find(|a| a.slug == slug && matches!(a.status, WebAppStatus::Published))
|
||||||
|
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||||
|
.clone();
|
||||||
|
drop(apps);
|
||||||
|
|
||||||
|
let pages = state.pages.read().await;
|
||||||
|
let index_page = pages.values().find(|p| p.app_id == app.id && p.is_index);
|
||||||
|
|
||||||
|
let content = index_page
|
||||||
|
.map(|p| p.content.clone())
|
||||||
|
.unwrap_or_else(|| "<p>Page not found</p>".to_string());
|
||||||
|
|
||||||
|
let html = render_html(&app, &content);
|
||||||
|
Ok(Html(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_page(
|
||||||
|
State(state): State<Arc<WebaState>>,
|
||||||
|
Path((slug, path)): Path<(String, String)>,
|
||||||
|
) -> Result<impl IntoResponse, axum::http::StatusCode> {
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
let app = apps
|
||||||
|
.values()
|
||||||
|
.find(|a| a.slug == slug && matches!(a.status, WebAppStatus::Published))
|
||||||
|
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||||
|
.clone();
|
||||||
|
drop(apps);
|
||||||
|
|
||||||
|
let normalized_path = format!("/{}", path.trim_start_matches('/'));
|
||||||
|
|
||||||
|
let pages = state.pages.read().await;
|
||||||
|
let page = pages
|
||||||
|
.values()
|
||||||
|
.find(|p| p.app_id == app.id && p.path == normalized_path);
|
||||||
|
|
||||||
|
let content = page
|
||||||
|
.map(|p| p.content.clone())
|
||||||
|
.unwrap_or_else(|| "<p>Page not found</p>".to_string());
|
||||||
|
|
||||||
|
let html = render_html(&app, &content);
|
||||||
|
Ok(Html(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_html(app: &WebApp, content: &str) -> String {
|
||||||
|
let meta_tags: String = app
|
||||||
|
.config
|
||||||
|
.meta_tags
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("<meta name=\"{}\" content=\"{}\">", k, v))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n ");
|
||||||
|
|
||||||
|
let scripts: String = app
|
||||||
|
.config
|
||||||
|
.scripts
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("<script src=\"{}\"></script>", s))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n ");
|
||||||
|
|
||||||
|
let styles: String = app
|
||||||
|
.config
|
||||||
|
.styles
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("<link rel=\"stylesheet\" href=\"{}\">", s))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n ");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{}</title>
|
||||||
|
{}
|
||||||
|
{}
|
||||||
|
<style>
|
||||||
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{}
|
||||||
|
{}
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
app.name, meta_tags, styles, content, scripts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slugify(s: &str) -> String {
|
||||||
|
s.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.split('-')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("-")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
// Placeholder for weba initialization
|
log::info!("WEBA module initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_slugify() {
|
||||||
|
assert_eq!(slugify("Hello World"), "hello-world");
|
||||||
|
assert_eq!(slugify("My App 123"), "my-app-123");
|
||||||
|
assert_eq!(slugify(" Test App "), "test-app");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_webapp_creation() {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let app = WebApp {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name: "Test App".to_string(),
|
||||||
|
slug: "test-app".to_string(),
|
||||||
|
description: None,
|
||||||
|
template: WebAppTemplate::Blank,
|
||||||
|
status: WebAppStatus::Draft,
|
||||||
|
config: WebAppConfig::default(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
assert_eq!(app.name, "Test App");
|
||||||
|
assert_eq!(app.slug, "test-app");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_weba_state() {
|
||||||
|
let state = WebaState::new();
|
||||||
|
let apps = state.apps.read().await;
|
||||||
|
assert!(apps.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue