botserver/src/basic/keywords/use_account.rs

226 lines
7.1 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 account = match account {
Some(acc) => acc,
None => {
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 path.starts_with("account://") {
let rest = &path[10..];
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>,
}