botserver/src/basic/keywords/use_account.rs
Rodrigo Rodriguez (Pragmatismo) 5da86bbef2 Fix clippy warnings: match arms, async/await, Debug impls, formatting
- Fix match arms with identical bodies by consolidating patterns
- Fix case-insensitive file extension comparisons using eq_ignore_ascii_case
- Fix unnecessary Debug formatting in log/format macros
- Fix clone_from usage instead of clone assignment
- Fix let...else patterns where appropriate
- Fix format! append to String using write! macro
- Fix unwrap_or with function calls to use unwrap_or_else
- Add missing fields to manual Debug implementations
- Fix duplicate code in if blocks
- Add type aliases for complex types
- Rename struct fields to avoid common prefixes
- Various other clippy warning fixes

Note: Some 'unused async' warnings remain for functions that are
called with .await but don't contain await internally - these are
kept async for API compatibility.
2025-12-26 08:59:25 -03:00

249 lines
7.8 KiB
Rust

use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use diesel::prelude::*;
use log::{error, info};
use rhai::{Dynamic, Engine, EvalAltResult};
use std::sync::Arc;
use uuid::Uuid;
#[derive(QueryableByName)]
#[allow(dead_code)]
struct AccountResult {
#[diesel(sql_type = diesel::sql_types::Uuid)]
id: Uuid,
#[diesel(sql_type = diesel::sql_types::Text)]
email: String,
#[diesel(sql_type = diesel::sql_types::Text)]
provider: String,
#[diesel(sql_type = diesel::sql_types::Text)]
account_type: String,
}
#[derive(QueryableByName, Debug, Clone)]
pub struct ActiveAccountResult {
#[diesel(sql_type = diesel::sql_types::Uuid)]
pub account_id: Uuid,
#[diesel(sql_type = diesel::sql_types::Text)]
pub email: String,
#[diesel(sql_type = diesel::sql_types::Text)]
pub provider: String,
#[diesel(sql_type = diesel::sql_types::Text)]
pub qdrant_collection: String,
}
pub fn register_use_account_keyword(
engine: &mut Engine,
state: Arc<AppState>,
session: Arc<UserSession>,
) -> Result<(), Box<EvalAltResult>> {
let state_clone = Arc::clone(&state);
let session_clone = Arc::clone(&session);
engine.register_custom_syntax(
["USE", "ACCOUNT", "$expr$"],
true,
move |context, inputs| {
let email = context.eval_expression_tree(&inputs[0])?.to_string();
info!(
"USE ACCOUNT keyword executed - Email: {}, Session: {}",
email, session_clone.id
);
let session_id = session_clone.id;
let bot_id = session_clone.bot_id;
let user_id = session_clone.user_id;
let conn = state_clone.conn.clone();
let email_clone = email.clone();
let result = std::thread::spawn(move || {
add_account_to_session(conn, session_id, bot_id, user_id, &email_clone)
})
.join();
match result {
Ok(Ok(_)) => {
info!("Account '{}' added to session {}", email, session_clone.id);
Ok(Dynamic::UNIT)
}
Ok(Err(e)) => {
error!("Failed to add account '{}': {}", email, e);
Err(format!("USE_ACCOUNT failed: {}", e).into())
}
Err(e) => {
error!("Thread panic in USE_ACCOUNT: {:?}", e);
Err("USE_ACCOUNT failed: thread panic".into())
}
}
},
)?;
Ok(())
}
fn add_account_to_session(
conn_pool: crate::shared::utils::DbPool,
session_id: Uuid,
bot_id: Uuid,
user_id: Uuid,
email: &str,
) -> Result<(), String> {
let mut conn = conn_pool
.get()
.map_err(|e| format!("Failed to get DB connection: {}", e))?;
let account: Option<AccountResult> = diesel::sql_query(
"SELECT id, email, provider, account_type FROM connected_accounts
WHERE email = $1 AND (bot_id = $2 OR user_id = $3) AND status = 'active'",
)
.bind::<diesel::sql_types::Text, _>(email)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.bind::<diesel::sql_types::Uuid, _>(user_id)
.get_result(&mut conn)
.optional()
.map_err(|e| format!("Failed to query account: {}", e))?;
let Some(account) = account else {
return Err(format!(
"Account '{}' not found or not configured. Add it in Sources app.",
email
));
};
let qdrant_collection = format!("account_{}_{}", account.provider, account.id);
let assoc_id = Uuid::new_v4();
diesel::sql_query(
"INSERT INTO session_account_associations
(id, session_id, bot_id, account_id, email, provider, qdrant_collection, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, true)
ON CONFLICT (session_id, account_id)
DO UPDATE SET is_active = true, added_at = NOW()",
)
.bind::<diesel::sql_types::Uuid, _>(assoc_id)
.bind::<diesel::sql_types::Uuid, _>(session_id)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.bind::<diesel::sql_types::Uuid, _>(account.id)
.bind::<diesel::sql_types::Text, _>(&account.email)
.bind::<diesel::sql_types::Text, _>(&account.provider)
.bind::<diesel::sql_types::Text, _>(&qdrant_collection)
.execute(&mut conn)
.map_err(|e| format!("Failed to add account association: {}", e))?;
info!(
"Added account '{}' ({}) to session {} (collection: {})",
email, account.provider, session_id, qdrant_collection
);
Ok(())
}
pub fn get_active_accounts_for_session(
conn_pool: &crate::shared::utils::DbPool,
session_id: Uuid,
) -> Result<Vec<ActiveAccountResult>, String> {
let mut conn = conn_pool
.get()
.map_err(|e| format!("Failed to get DB connection: {}", e))?;
let results: Vec<ActiveAccountResult> = diesel::sql_query(
"SELECT account_id, email, provider, qdrant_collection
FROM session_account_associations
WHERE session_id = $1 AND is_active = true
ORDER BY added_at DESC",
)
.bind::<diesel::sql_types::Uuid, _>(session_id)
.load(&mut conn)
.map_err(|e| format!("Failed to get active accounts: {}", e))?;
Ok(results)
}
pub fn parse_account_path(path: &str) -> Option<(String, String)> {
if let Some(rest) = path.strip_prefix("account://") {
if let Some(slash_pos) = rest.find('/') {
let email = &rest[..slash_pos];
let file_path = &rest[slash_pos + 1..];
return Some((email.to_string(), file_path.to_string()));
}
}
None
}
pub fn is_account_path(path: &str) -> bool {
path.starts_with("account://")
}
pub async fn get_account_credentials(
conn_pool: &crate::shared::utils::DbPool,
email: &str,
bot_id: Uuid,
) -> Result<AccountCredentials, String> {
let mut conn = conn_pool
.get()
.map_err(|e| format!("Failed to get DB connection: {}", e))?;
#[derive(QueryableByName)]
struct CredResult {
#[diesel(sql_type = diesel::sql_types::Uuid)]
id: Uuid,
#[diesel(sql_type = diesel::sql_types::Text)]
provider: String,
#[diesel(sql_type = diesel::sql_types::Text)]
access_token: String,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
refresh_token: Option<String>,
}
let creds: CredResult = diesel::sql_query(
"SELECT id, provider, access_token, refresh_token
FROM connected_accounts
WHERE email = $1 AND bot_id = $2 AND status = 'active'",
)
.bind::<diesel::sql_types::Text, _>(email)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.get_result(&mut conn)
.map_err(|e| format!("Account not found: {}", e))?;
Ok(AccountCredentials {
account_id: creds.id,
provider: creds.provider,
access_token: creds.access_token,
refresh_token: creds.refresh_token,
})
}
#[derive(Debug, Clone)]
pub struct AccountCredentials {
pub account_id: Uuid,
pub provider: String,
pub access_token: String,
pub refresh_token: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_account_path() {
let result = parse_account_path("account://user@gmail.com/Documents/file.pdf");
assert!(result.is_some());
let (email, path) = result.unwrap();
assert_eq!(email, "user@gmail.com");
assert_eq!(path, "Documents/file.pdf");
}
#[test]
fn test_parse_account_path_invalid() {
assert!(parse_account_path("local/file.pdf").is_none());
assert!(parse_account_path("/absolute/path").is_none());
}
#[test]
fn test_is_account_path() {
assert!(is_account_path("account://user@gmail.com/file.pdf"));
assert!(!is_account_path("local/file.pdf"));
assert!(!is_account_path("file.pdf"));
}
}