botserver/src/basic/keywords/on_change.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

277 lines
7.8 KiB
Rust

use log::info;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::path::Path;
use uuid::Uuid;
use crate::core::shared::state::AppState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FolderProvider {
GDrive,
OneDrive,
Dropbox,
Local,
}
impl std::str::FromStr for FolderProvider {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"gdrive" | "google" | "googledrive" => Ok(FolderProvider::GDrive),
"onedrive" | "microsoft" => Ok(FolderProvider::OneDrive),
"dropbox" => Ok(FolderProvider::Dropbox),
"local" | "filesystem" => Ok(FolderProvider::Local),
_ => Err(()),
}
}
}
impl std::fmt::Display for FolderProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FolderProvider::GDrive => write!(f, "gdrive"),
FolderProvider::OneDrive => write!(f, "onedrive"),
FolderProvider::Dropbox => write!(f, "dropbox"),
FolderProvider::Local => write!(f, "local"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderMonitor {
pub id: Uuid,
pub user_id: Uuid,
pub org_id: Uuid,
pub provider: FolderProvider,
pub folder_path: String,
pub folder_id: Option<String>,
pub recursive: bool,
pub event_types: Vec<String>,
pub script_path: String,
pub enabled: bool,
pub last_check: Option<chrono::DateTime<chrono::Utc>>,
pub last_token: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderChangeEvent {
pub path: String,
pub event_type: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub size: Option<i64>,
pub is_directory: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnChangeConfig {
pub provider: FolderProvider,
pub folder_path: String,
pub folder_id: Option<String>,
pub recursive: bool,
pub event_types: Vec<String>,
pub filters: Option<FileFilters>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileFilters {
pub extensions: Option<Vec<String>>,
pub min_size: Option<i64>,
pub max_size: Option<i64>,
pub name_pattern: Option<String>,
}
pub struct OnChangeKeyword;
impl OnChangeKeyword {
pub fn execute(
_state: &AppState,
config: OnChangeConfig,
callback_script: &str,
) -> Result<Value, String> {
info!(
"Setting up folder monitor for {:?} at {}",
config.provider, config.folder_path
);
let monitor = FolderMonitor {
id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
org_id: Uuid::new_v4(),
provider: config.provider,
folder_path: config.folder_path.clone(),
folder_id: config.folder_id.clone(),
recursive: config.recursive,
event_types: config.event_types.clone(),
script_path: callback_script.to_string(),
enabled: true,
last_check: None,
last_token: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
Ok(json!({
"success": true,
"monitor_id": monitor.id,
"provider": monitor.provider.to_string(),
"folder_path": monitor.folder_path,
"message": "Folder monitor configured (simulation mode)"
}))
}
pub fn check_changes(
state: &AppState,
monitor_id: Uuid,
) -> Result<Vec<FolderChangeEvent>, String> {
info!("Checking for folder changes for monitor {}", monitor_id);
fetch_folder_changes(state, monitor_id)
}
pub fn stop_monitor(monitor_id: Uuid) -> Result<Value, String> {
info!("Stopping folder monitor {}", monitor_id);
Ok(json!({
"success": true,
"monitor_id": monitor_id,
"message": "Monitor stopped"
}))
}
}
pub fn fetch_folder_changes(
_state: &AppState,
_monitor_id: Uuid,
) -> Result<Vec<FolderChangeEvent>, String> {
let now = chrono::Utc::now();
let events = vec![
FolderChangeEvent {
path: "documents/report.pdf".to_string(),
event_type: "modified".to_string(),
timestamp: now,
size: Some(125000),
is_directory: false,
},
FolderChangeEvent {
path: "documents/new_file.docx".to_string(),
event_type: "created".to_string(),
timestamp: now,
size: Some(45000),
is_directory: false,
},
];
info!(
"Folder change check: returning {} simulated events (real APIs require OAuth setup)",
events.len()
);
Ok(events)
}
#[allow(dead_code)]
fn apply_filters(events: Vec<FolderChangeEvent>, filters: &Option<FileFilters>) -> Vec<FolderChangeEvent> {
let Some(filters) = filters else {
return events;
};
events
.into_iter()
.filter(|event| {
if let Some(ref extensions) = filters.extensions {
let ext = Path::new(&event.path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if !extensions.iter().any(|e| e.eq_ignore_ascii_case(ext)) {
return false;
}
}
if let Some(min_size) = filters.min_size {
if event.size.unwrap_or(0) < min_size {
return false;
}
}
if let Some(max_size) = filters.max_size {
if event.size.unwrap_or(i64::MAX) > max_size {
return false;
}
}
if let Some(ref pattern) = filters.name_pattern {
let file_name = Path::new(&event.path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if !file_name.contains(pattern) {
return false;
}
}
true
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_folder_provider_from_str() {
assert_eq!(
"gdrive".parse::<FolderProvider>().ok(),
Some(FolderProvider::GDrive)
);
assert_eq!(
"onedrive".parse::<FolderProvider>().ok(),
Some(FolderProvider::OneDrive)
);
assert_eq!(
"dropbox".parse::<FolderProvider>().ok(),
Some(FolderProvider::Dropbox)
);
assert_eq!(
"local".parse::<FolderProvider>().ok(),
Some(FolderProvider::Local)
);
}
#[test]
fn test_apply_filters_extension() {
let events = vec![
FolderChangeEvent {
path: "test.pdf".to_string(),
event_type: "created".to_string(),
timestamp: chrono::Utc::now(),
size: Some(1000),
is_directory: false,
},
FolderChangeEvent {
path: "test.txt".to_string(),
event_type: "created".to_string(),
timestamp: chrono::Utc::now(),
size: Some(500),
is_directory: false,
},
];
let filters = Some(FileFilters {
extensions: Some(vec!["pdf".to_string()]),
min_size: None,
max_size: None,
name_pattern: None,
});
let filtered = apply_filters(events, &filters);
assert_eq!(filtered.len(), 1);
assert!(filtered[0].path.ends_with(".pdf"));
}
}