mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
1b71dc721c
Introduce OmoVariant struct with STANDARD/SLIM constants to eliminate ~250 lines of copy-pasted code across DAO, service, commands, and frontend layers. Adding a new OMO variant now requires only a single const declaration instead of duplicating ~400 lines.
492 lines
18 KiB
Rust
492 lines
18 KiB
Rust
use crate::database::{lock_conn, Database};
|
|
use crate::error::AppError;
|
|
use crate::provider::{Provider, ProviderMeta};
|
|
use indexmap::IndexMap;
|
|
use rusqlite::params;
|
|
use std::collections::HashMap;
|
|
|
|
type OmoProviderRow = (
|
|
String,
|
|
String,
|
|
String,
|
|
Option<String>,
|
|
Option<i64>,
|
|
Option<usize>,
|
|
Option<String>,
|
|
String,
|
|
);
|
|
|
|
impl Database {
|
|
pub fn get_all_providers(
|
|
&self,
|
|
app_type: &str,
|
|
) -> Result<IndexMap<String, Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn.prepare(
|
|
"SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
|
FROM providers WHERE app_type = ?1
|
|
ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC"
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let provider_iter = stmt
|
|
.query_map(params![app_type], |row| {
|
|
let id: String = row.get(0)?;
|
|
let name: String = row.get(1)?;
|
|
let settings_config_str: String = row.get(2)?;
|
|
let website_url: Option<String> = row.get(3)?;
|
|
let category: Option<String> = row.get(4)?;
|
|
let created_at: Option<i64> = row.get(5)?;
|
|
let sort_index: Option<usize> = row.get(6)?;
|
|
let notes: Option<String> = row.get(7)?;
|
|
let icon: Option<String> = row.get(8)?;
|
|
let icon_color: Option<String> = row.get(9)?;
|
|
let meta_str: String = row.get(10)?;
|
|
let in_failover_queue: bool = row.get(11)?;
|
|
|
|
let settings_config =
|
|
serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
|
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
|
|
|
Ok((
|
|
id,
|
|
Provider {
|
|
id: "".to_string(), // Placeholder, set below
|
|
name,
|
|
settings_config,
|
|
website_url,
|
|
category,
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon,
|
|
icon_color,
|
|
in_failover_queue,
|
|
},
|
|
))
|
|
})
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut providers = IndexMap::new();
|
|
for provider_res in provider_iter {
|
|
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
|
provider.id = id.clone();
|
|
|
|
let mut stmt_endpoints = conn.prepare(
|
|
"SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC"
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let endpoints_iter = stmt_endpoints
|
|
.query_map(params![id, app_type], |row| {
|
|
let url: String = row.get(0)?;
|
|
let added_at: Option<i64> = row.get(1)?;
|
|
Ok((
|
|
url,
|
|
crate::settings::CustomEndpoint {
|
|
url: "".to_string(),
|
|
added_at: added_at.unwrap_or(0),
|
|
last_used: None,
|
|
},
|
|
))
|
|
})
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut custom_endpoints = HashMap::new();
|
|
for ep_res in endpoints_iter {
|
|
let (url, mut ep) = ep_res.map_err(|e| AppError::Database(e.to_string()))?;
|
|
ep.url = url.clone();
|
|
custom_endpoints.insert(url, ep);
|
|
}
|
|
|
|
if let Some(meta) = &mut provider.meta {
|
|
meta.custom_endpoints = custom_endpoints;
|
|
}
|
|
|
|
providers.insert(id, provider);
|
|
}
|
|
|
|
Ok(providers)
|
|
}
|
|
|
|
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let mut stmt = conn
|
|
.prepare("SELECT id FROM providers WHERE app_type = ?1 AND is_current = 1 LIMIT 1")
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut rows = stmt
|
|
.query(params![app_type])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
|
|
Ok(Some(
|
|
row.get(0).map_err(|e| AppError::Database(e.to_string()))?,
|
|
))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn get_provider_by_id(
|
|
&self,
|
|
id: &str,
|
|
app_type: &str,
|
|
) -> Result<Option<Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let result = conn.query_row(
|
|
"SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
|
FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
|row| {
|
|
let name: String = row.get(0)?;
|
|
let settings_config_str: String = row.get(1)?;
|
|
let website_url: Option<String> = row.get(2)?;
|
|
let category: Option<String> = row.get(3)?;
|
|
let created_at: Option<i64> = row.get(4)?;
|
|
let sort_index: Option<usize> = row.get(5)?;
|
|
let notes: Option<String> = row.get(6)?;
|
|
let icon: Option<String> = row.get(7)?;
|
|
let icon_color: Option<String> = row.get(8)?;
|
|
let meta_str: String = row.get(9)?;
|
|
let in_failover_queue: bool = row.get(10)?;
|
|
|
|
let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
|
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
|
|
|
Ok(Provider {
|
|
id: id.to_string(),
|
|
name,
|
|
settings_config,
|
|
website_url,
|
|
category,
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon,
|
|
icon_color,
|
|
in_failover_queue,
|
|
})
|
|
},
|
|
);
|
|
|
|
match result {
|
|
Ok(provider) => Ok(Some(provider)),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
Err(e) => Err(AppError::Database(e.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
|
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
|
|
|
let existing: Option<(bool, bool)> = tx
|
|
.query_row(
|
|
"SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![provider.id, app_type],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.ok();
|
|
|
|
let is_update = existing.is_some();
|
|
let (is_current, in_failover_queue) =
|
|
existing.unwrap_or((false, provider.in_failover_queue));
|
|
|
|
if is_update {
|
|
tx.execute(
|
|
"UPDATE providers SET
|
|
name = ?1,
|
|
settings_config = ?2,
|
|
website_url = ?3,
|
|
category = ?4,
|
|
created_at = ?5,
|
|
sort_index = ?6,
|
|
notes = ?7,
|
|
icon = ?8,
|
|
icon_color = ?9,
|
|
meta = ?10,
|
|
is_current = ?11,
|
|
in_failover_queue = ?12
|
|
WHERE id = ?13 AND app_type = ?14",
|
|
params![
|
|
provider.name,
|
|
serde_json::to_string(&provider.settings_config).map_err(|e| {
|
|
AppError::Database(format!("Failed to serialize settings_config: {e}"))
|
|
})?,
|
|
provider.website_url,
|
|
provider.category,
|
|
provider.created_at,
|
|
provider.sort_index,
|
|
provider.notes,
|
|
provider.icon,
|
|
provider.icon_color,
|
|
serde_json::to_string(&meta_clone).map_err(|e| AppError::Database(format!(
|
|
"Failed to serialize meta: {e}"
|
|
)))?,
|
|
is_current,
|
|
in_failover_queue,
|
|
provider.id,
|
|
app_type,
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
} else {
|
|
tx.execute(
|
|
"INSERT INTO providers (
|
|
id, app_type, name, settings_config, website_url, category,
|
|
created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue
|
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
|
params![
|
|
provider.id,
|
|
app_type,
|
|
provider.name,
|
|
serde_json::to_string(&provider.settings_config)
|
|
.map_err(|e| AppError::Database(format!("Failed to serialize settings_config: {e}")))?,
|
|
provider.website_url,
|
|
provider.category,
|
|
provider.created_at,
|
|
provider.sort_index,
|
|
provider.notes,
|
|
provider.icon,
|
|
provider.icon_color,
|
|
serde_json::to_string(&meta_clone)
|
|
.map_err(|e| AppError::Database(format!("Failed to serialize meta: {e}")))?,
|
|
is_current,
|
|
in_failover_queue,
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
for (url, endpoint) in endpoints {
|
|
tx.execute(
|
|
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
|
VALUES (?1, ?2, ?3, ?4)",
|
|
params![provider.id, app_type, url, endpoint.added_at],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
}
|
|
}
|
|
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"DELETE FROM providers WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
|
params![app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
|
params![id, app_type],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_provider_settings_config(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
settings_config: &serde_json::Value,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"UPDATE providers SET settings_config = ?1 WHERE id = ?2 AND app_type = ?3",
|
|
params![
|
|
serde_json::to_string(settings_config).map_err(|e| AppError::Database(format!(
|
|
"Failed to serialize settings_config: {e}"
|
|
)))?,
|
|
provider_id,
|
|
app_type
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn add_custom_endpoint(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
url: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let added_at = chrono::Utc::now().timestamp_millis();
|
|
conn.execute(
|
|
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)",
|
|
params![provider_id, app_type, url, added_at],
|
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove_custom_endpoint(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
url: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"DELETE FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 AND url = ?3",
|
|
params![provider_id, app_type, url],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<(), AppError> {
|
|
let mut conn = lock_conn!(self.conn);
|
|
let tx = conn
|
|
.transaction()
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
tx.execute(
|
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2",
|
|
params![app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
let updated = tx
|
|
.execute(
|
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
if updated != 1 {
|
|
return Err(AppError::Database(format!(
|
|
"Failed to set {category} provider current: provider '{provider_id}' not found in app '{app_type}'"
|
|
)));
|
|
}
|
|
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn is_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<bool, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
match conn.query_row(
|
|
"SELECT is_current FROM providers
|
|
WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
|row| row.get(0),
|
|
) {
|
|
Ok(is_current) => Ok(is_current),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
|
|
Err(e) => Err(AppError::Database(e.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn clear_omo_provider_current(
|
|
&self,
|
|
app_type: &str,
|
|
provider_id: &str,
|
|
category: &str,
|
|
) -> Result<(), AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
conn.execute(
|
|
"UPDATE providers SET is_current = 0
|
|
WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
|
params![provider_id, app_type, category],
|
|
)
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_current_omo_provider(
|
|
&self,
|
|
app_type: &str,
|
|
category: &str,
|
|
) -> Result<Option<Provider>, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let row_data: Result<OmoProviderRow, rusqlite::Error> = conn.query_row(
|
|
"SELECT id, name, settings_config, category, created_at, sort_index, notes, meta
|
|
FROM providers
|
|
WHERE app_type = ?1 AND category = ?2 AND is_current = 1
|
|
LIMIT 1",
|
|
params![app_type, category],
|
|
|row| {
|
|
Ok((
|
|
row.get(0)?,
|
|
row.get(1)?,
|
|
row.get(2)?,
|
|
row.get(3)?,
|
|
row.get(4)?,
|
|
row.get(5)?,
|
|
row.get(6)?,
|
|
row.get(7)?,
|
|
))
|
|
},
|
|
);
|
|
|
|
let (id, name, settings_config_str, _row_category, created_at, sort_index, notes, meta_str) =
|
|
match row_data {
|
|
Ok(v) => v,
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
|
Err(e) => return Err(AppError::Database(e.to_string())),
|
|
};
|
|
|
|
let settings_config = serde_json::from_str(&settings_config_str).map_err(|e| {
|
|
AppError::Database(format!(
|
|
"Failed to parse {category} provider settings_config (provider_id={id}): {e}"
|
|
))
|
|
})?;
|
|
let meta: crate::provider::ProviderMeta = if meta_str.trim().is_empty() {
|
|
crate::provider::ProviderMeta::default()
|
|
} else {
|
|
serde_json::from_str(&meta_str).map_err(|e| {
|
|
AppError::Database(format!(
|
|
"Failed to parse {category} provider meta (provider_id={id}): {e}"
|
|
))
|
|
})?
|
|
};
|
|
|
|
Ok(Some(Provider {
|
|
id,
|
|
name,
|
|
settings_config,
|
|
website_url: None,
|
|
category: Some(category.to_string()),
|
|
created_at,
|
|
sort_index,
|
|
notes,
|
|
meta: Some(meta),
|
|
icon: None,
|
|
icon_color: None,
|
|
in_failover_queue: false,
|
|
}))
|
|
}
|
|
}
|