botserver/src/paper/handlers.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

272 lines
8.6 KiB
Rust

use crate::core::shared::state::AppState;
use crate::core::urls::ApiUrls;
use axum::{
extract::{Path, Query, State},
http::HeaderMap,
response::{Html, IntoResponse},
Json,
};
use std::sync::Arc;
use uuid::Uuid;
use super::auth::get_current_user;
use super::models::SaveRequest;
use super::storage::{delete_document_from_drive, list_documents_from_drive, load_document_from_drive, save_document_to_drive};
use super::utils::{format_document_content, format_document_list_item, format_error, format_relative_time};
pub async fn handle_new_document(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let (user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
let doc_id = Uuid::new_v4().to_string();
let title = "Untitled".to_string();
let content = String::new();
if let Err(e) =
save_document_to_drive(&state, &user_identifier, &doc_id, &title, &content, false).await
{
log::error!("Failed to save new document: {}", e);
}
let mut html = String::new();
html.push_str("<div class=\"paper-new-created\" data-id=\"");
html.push_str(&super::utils::html_escape(&doc_id));
html.push_str("\">");
html.push_str(&format_document_list_item(
&doc_id, &title, "just now", true,
));
html.push_str("<script>");
html.push_str("htmx.trigger('#paper-list', 'refresh');");
html.push_str(&format!("htmx.ajax('GET', '{}', {{target: '#editor-content', swap: 'innerHTML'}});",
ApiUrls::PAPER_BY_ID.replace(":id", &super::utils::html_escape(&doc_id))));
html.push_str("</script>");
html.push_str("</div>");
log::info!("New document created: {} for user {}", doc_id, user_id);
Html(html)
}
pub async fn handle_list_documents(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
let documents = match list_documents_from_drive(&state, &user_identifier).await {
Ok(docs) => docs,
Err(e) => {
log::error!("Failed to list documents: {}", e);
Vec::new()
}
};
let mut html = String::new();
html.push_str("<div class=\"paper-list\">");
if documents.is_empty() {
html.push_str("<div class=\"paper-empty\">");
html.push_str("<p>No documents yet</p>");
html.push_str(&format!("<button class=\"btn-new\" hx-post=\"{}\" hx-target=\"#paper-list\" hx-swap=\"afterbegin\">Create your first document</button>", ApiUrls::PAPER_NEW));
html.push_str("</div>");
} else {
for doc in documents {
let time_str = format_relative_time(doc.updated_at);
let badge = if doc.storage_type == "named" {
" 📁"
} else {
""
};
html.push_str(&format_document_list_item(
&doc.id,
&format!("{}{}", doc.title, badge),
&time_str,
false,
));
}
}
html.push_str("</div>");
Html(html)
}
pub async fn handle_search_documents(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Query(params): Query<super::models::SearchQuery>,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
let query = params.q.unwrap_or_default().to_lowercase();
let documents = list_documents_from_drive(&state, &user_identifier)
.await
.unwrap_or_default();
let filtered: Vec<_> = if query.is_empty() {
documents
} else {
documents
.into_iter()
.filter(|d| d.title.to_lowercase().contains(&query))
.collect()
};
let mut html = String::new();
html.push_str("<div class=\"paper-search-results\">");
if filtered.is_empty() {
html.push_str("<div class=\"paper-empty\">");
html.push_str("<p>No documents found</p>");
html.push_str("</div>");
} else {
for doc in filtered {
let time_str = format_relative_time(doc.updated_at);
html.push_str(&format_document_list_item(
&doc.id, &doc.title, &time_str, false,
));
}
}
html.push_str("</div>");
Html(html)
}
pub async fn handle_get_document(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
match load_document_from_drive(&state, &user_identifier, &id).await {
Ok(Some(doc)) => Html(format_document_content(&doc.title, &doc.content)),
Ok(None) => Html(format_document_content("Untitled", "")),
Err(e) => {
log::error!("Failed to load document {}: {}", id, e);
Html(format_document_content("Untitled", ""))
}
}
}
pub async fn handle_save_document(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<SaveRequest>,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
let doc_id = payload.id.unwrap_or_else(|| Uuid::new_v4().to_string());
let title = payload.title.unwrap_or_else(|| "Untitled".to_string());
let content = payload.content.unwrap_or_default();
let is_named = payload.save_as_named.unwrap_or(false);
match save_document_to_drive(
&state,
&user_identifier,
&doc_id,
&title,
&content,
is_named,
)
.await
{
Ok(path) => {
log::info!("Document saved: {} at {}", doc_id, path);
let mut html = String::new();
html.push_str("<div class=\"save-success\">");
html.push_str("<span class=\"save-icon\">*</span>");
html.push_str("<span>Saved</span>");
html.push_str("</div>");
Html(html)
}
Err(e) => {
log::error!("Failed to save document: {}", e);
Html(format_error("Failed to save document"))
}
}
}
pub async fn handle_autosave(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<SaveRequest>,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(String::new());
}
};
let doc_id = payload.id.unwrap_or_else(|| Uuid::new_v4().to_string());
let title = payload.title.unwrap_or_else(|| "Untitled".to_string());
let content = payload.content.unwrap_or_default();
if let Err(e) =
save_document_to_drive(&state, &user_identifier, &doc_id, &title, &content, false).await
{
log::warn!("Autosave failed for {}: {}", doc_id, e);
}
Html("<span class=\"autosave-indicator\">Auto-saved</span>".to_string())
}
pub async fn handle_delete_document(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> impl IntoResponse {
let (_user_id, user_identifier) = match get_current_user(&state, &headers).await {
Ok(u) => u,
Err(e) => {
log::error!("Auth error: {}", e);
return Html(format_error("Authentication required"));
}
};
match delete_document_from_drive(&state, &user_identifier, &id).await {
Ok(()) => {
log::info!("Document deleted: {}", id);
Html(format!("<div class=\"delete-success\" hx-trigger=\"load\" hx-get=\"{}\" hx-target=\"#paper-list\" hx-swap=\"innerHTML\"></div>", ApiUrls::PAPER_LIST))
}
Err(e) => {
log::error!("Failed to delete document {}: {}", id, e);
Html(format_error("Failed to delete document"))
}
}
}