mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 05:12:03 +08:00
feat(antigravity): add Google Antigravity auth plugin with auto-rotation, model catalog and multi-turn support
This commit is contained in:
@@ -125,6 +125,7 @@ impl App {
|
||||
self.registry.web_cache().set_service_addr(address);
|
||||
self.harness.set_backend_addr(address);
|
||||
tracing::info!(%address, "cursor server listening");
|
||||
tokio::spawn(crate::plugin::oauth_callback::start_oauth_callback_server(shutdown.clone()));
|
||||
let registry = self.registry;
|
||||
let harness = self.harness;
|
||||
let graceful = shutdown.clone();
|
||||
|
||||
@@ -138,6 +138,14 @@ pub fn api_router(service: ControlService) -> Router {
|
||||
.route("/__byok-api__/api/llm-calls", get(calls::list))
|
||||
.route("/__byok-api__/api/llm-calls/{call_id}", get(calls::detail))
|
||||
.route("/__byok-api__/api/plugins", get(plugins::list))
|
||||
.route(
|
||||
"/__byok-api__/api/plugins/disabled-models",
|
||||
get(plugins::get_disabled_models).put(plugins::set_disabled_models),
|
||||
)
|
||||
.route(
|
||||
"/__byok-api__/api/plugins/disabled-accounts",
|
||||
get(plugins::get_disabled_accounts).put(plugins::set_disabled_accounts),
|
||||
)
|
||||
.route(
|
||||
"/__byok-api__/api/plugins/runtime",
|
||||
get(plugins::runtime_status)
|
||||
|
||||
@@ -4,6 +4,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
plugin::{
|
||||
@@ -122,3 +123,45 @@ pub async fn cancel_runtime_initialization(
|
||||
) -> Result<Json<PluginRuntimeStatus>> {
|
||||
Ok(Json(service.cancel_plugin_runtime_initialization()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetDisabledModelsInput {
|
||||
pub model_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn get_disabled_models(
|
||||
State(service): State<ControlService>,
|
||||
) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(service.disabled_plugin_models().await?))
|
||||
}
|
||||
|
||||
pub async fn set_disabled_models(
|
||||
State(service): State<ControlService>,
|
||||
Json(input): Json<SetDisabledModelsInput>,
|
||||
) -> Result<Json<Vec<String>>> {
|
||||
service.set_disabled_plugin_models(input.model_ids).await?;
|
||||
Ok(Json(service.disabled_plugin_models().await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetDisabledAccountsInput {
|
||||
pub account_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn get_disabled_accounts(
|
||||
State(service): State<ControlService>,
|
||||
) -> Result<Json<Vec<String>>> {
|
||||
Ok(Json(service.disabled_plugin_accounts().await?))
|
||||
}
|
||||
|
||||
pub async fn set_disabled_accounts(
|
||||
State(service): State<ControlService>,
|
||||
Json(input): Json<SetDisabledAccountsInput>,
|
||||
) -> Result<Json<Vec<String>>> {
|
||||
service
|
||||
.set_disabled_plugin_accounts(input.account_ids)
|
||||
.await?;
|
||||
Ok(Json(service.disabled_plugin_accounts().await?))
|
||||
}
|
||||
|
||||
@@ -251,6 +251,28 @@ impl ControlService {
|
||||
self.plugin_runtime.cancel_initialization()
|
||||
}
|
||||
|
||||
pub async fn disabled_plugin_models(&self) -> Result<Vec<String>> {
|
||||
let mut list: Vec<_> = self.store.disabled_plugin_models().await?.into_iter().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub async fn set_disabled_plugin_models(&self, model_ids: Vec<String>) -> Result<()> {
|
||||
let set = model_ids.into_iter().collect();
|
||||
self.store.set_disabled_plugin_models(&set).await
|
||||
}
|
||||
|
||||
pub async fn disabled_plugin_accounts(&self) -> Result<Vec<String>> {
|
||||
let mut list: Vec<_> = self.store.disabled_plugin_accounts().await?.into_iter().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub async fn set_disabled_plugin_accounts(&self, account_ids: Vec<String>) -> Result<()> {
|
||||
let set = account_ids.into_iter().collect();
|
||||
self.store.set_disabled_plugin_accounts(&set).await
|
||||
}
|
||||
|
||||
pub(super) async fn ads(
|
||||
&self,
|
||||
disabled_ad_ids: Option<&str>,
|
||||
|
||||
@@ -109,7 +109,63 @@ const GROK_AUTH: &[(&str, &str)] = &[
|
||||
),
|
||||
];
|
||||
|
||||
const PLUGINS: &[(&str, &[(&str, &str)])] = &[("codex-auth", CODEX_AUTH), ("grok-auth", GROK_AUTH)];
|
||||
const ANTIGRAVITY_AUTH: &[(&str, &str)] = &[
|
||||
(
|
||||
"plugin.json",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/plugin.json"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"main.ts",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/main.ts"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"provider.ts",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/provider.ts"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"models.ts",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/models.ts"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"oauth.ts",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/oauth.ts"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"resources.ts",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/resources.ts"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"assets/antigravity.svg",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/plugins/build-in/antigravity-auth/assets/antigravity.svg"
|
||||
)),
|
||||
),
|
||||
];
|
||||
|
||||
const PLUGINS: &[(&str, &[(&str, &str)])] = &[
|
||||
("antigravity-auth", ANTIGRAVITY_AUTH),
|
||||
("codex-auth", CODEX_AUTH),
|
||||
("grok-auth", GROK_AUTH),
|
||||
];
|
||||
|
||||
/// 把内置插件预装到 installed 目录。manifest 的 version 是缓存键:
|
||||
/// 版本一致时零写盘;版本变化时整目录同步并清理旧版本残留文件。
|
||||
|
||||
@@ -203,20 +203,20 @@ fn validate_component(value: &str, label: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_directory_permissions(path: &Path) -> Result<()> {
|
||||
fn set_directory_permissions(_path: &Path) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
|
||||
std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_file_permissions(path: &Path) -> Result<()> {
|
||||
fn set_file_permissions(_path: &Path) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
|
||||
std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod definition;
|
||||
mod descriptor;
|
||||
mod installation;
|
||||
mod manifest;
|
||||
pub mod oauth_callback;
|
||||
mod protocol;
|
||||
mod registry;
|
||||
mod runtime;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Lightweight local OAuth callback server for Google / Antigravity OAuth redirect flows.
|
||||
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::Html,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct OAuthCallbackState {
|
||||
codes: Arc<RwLock<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CallbackQuery {
|
||||
pub code: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct StatusQuery {
|
||||
pub state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StatusResponse {
|
||||
pub code: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn start_oauth_callback_server(shutdown: tokio_util::sync::CancellationToken) {
|
||||
let port = 51121;
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
let state = OAuthCallbackState::default();
|
||||
|
||||
let router = Router::new()
|
||||
.route("/oauth-callback", get(handle_callback))
|
||||
.route("/auth-status", get(handle_status))
|
||||
.with_state(state);
|
||||
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(err) => {
|
||||
tracing::warn!(%addr, %err, "OAuth callback port 51121 unavailable or already bound");
|
||||
return;
|
||||
}
|
||||
};
|
||||
tracing::info!(%addr, "OAuth callback server listening");
|
||||
|
||||
let server = axum::serve(listener, router).with_graceful_shutdown(async move {
|
||||
shutdown.cancelled().await;
|
||||
});
|
||||
|
||||
if let Err(err) = server.await {
|
||||
tracing::debug!(%err, "OAuth callback server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_callback(
|
||||
State(state): State<OAuthCallbackState>,
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> Html<&'static str> {
|
||||
if let (Some(code), Some(st)) = (query.code, query.state) {
|
||||
state.codes.write().insert(st, code);
|
||||
}
|
||||
Html(r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Antigravity Authorization Successful</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #18181b; color: #f4f4f5; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.card { background: #27272a; border: 1px solid #3f3f46; padding: 36px; border-radius: 16px; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); max-width: 440px; }
|
||||
.icon { font-size: 40px; color: #4285f4; margin-bottom: 16px; }
|
||||
h1 { color: #ffffff; font-size: 20px; margin: 0 0 12px 0; }
|
||||
p { color: #a1a1aa; font-size: 14px; line-height: 1.6; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">✓</div>
|
||||
<h1>Authorization Successful</h1>
|
||||
<p>Your Google Antigravity account has been authorized. You can close this browser tab and return to <strong>Cursor BYOK</strong>.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#)
|
||||
}
|
||||
|
||||
async fn handle_status(
|
||||
State(state): State<OAuthCallbackState>,
|
||||
Query(query): Query<StatusQuery>,
|
||||
) -> Json<StatusResponse> {
|
||||
let code = query.state.and_then(|st| state.codes.read().get(&st).cloned());
|
||||
Json(StatusResponse { code })
|
||||
}
|
||||
@@ -40,6 +40,7 @@ struct RegistryInner {
|
||||
entries: RwLock<Option<Vec<PluginEntry>>>,
|
||||
workers: Mutex<HashMap<String, Arc<PluginWorker>>>,
|
||||
oauth_sessions: Mutex<HashMap<String, OAuthSession>>,
|
||||
rr_counter: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
struct OAuthSession {
|
||||
@@ -108,6 +109,7 @@ impl PluginRegistry {
|
||||
entries: RwLock::new(None),
|
||||
workers: Mutex::new(HashMap::new()),
|
||||
oauth_sessions: Mutex::new(HashMap::new()),
|
||||
rr_counter: std::sync::atomic::AtomicUsize::new(0),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -142,6 +144,12 @@ impl PluginRegistry {
|
||||
let Some(executable) = self.inner.runtime.executable() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let disabled_models = self
|
||||
.inner
|
||||
.store
|
||||
.disabled_plugin_models()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut models = Vec::new();
|
||||
for entry in self.entries(&executable).await {
|
||||
for provider in &entry.definition.providers {
|
||||
@@ -154,14 +162,19 @@ impl PluginRegistry {
|
||||
.models(&entry.manifest.id, &provider.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
models.extend(stored.iter().map(|model| {
|
||||
PluginModelDescriptor::new(
|
||||
models.extend(stored.iter().filter_map(|model| {
|
||||
let descriptor = PluginModelDescriptor::new(
|
||||
&entry.manifest.id,
|
||||
&entry.manifest.name,
|
||||
&entry.icon,
|
||||
provider,
|
||||
model,
|
||||
)
|
||||
);
|
||||
if disabled_models.contains(&descriptor.id) {
|
||||
None
|
||||
} else {
|
||||
Some(descriptor)
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -192,6 +205,15 @@ impl PluginRegistry {
|
||||
}
|
||||
|
||||
pub async fn plan_model(&self, model_id: &str) -> Result<PluginInvocationPlan> {
|
||||
let disabled_models = self
|
||||
.inner
|
||||
.store
|
||||
.disabled_plugin_models()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if disabled_models.contains(model_id) {
|
||||
return Err(Error::Provider(format!("plugin model '{model_id}' is disabled")));
|
||||
}
|
||||
let model = self.model_descriptor(model_id).await?;
|
||||
let request_url = format!("plugin://{}/{}", model.plugin_id, model.provider_id);
|
||||
Ok(PluginInvocationPlan { model, request_url })
|
||||
@@ -704,13 +726,21 @@ impl PluginRegistry {
|
||||
}
|
||||
}
|
||||
match &provider.resource_type {
|
||||
Some(resource_type) => !self
|
||||
.inner
|
||||
.state
|
||||
.resources(plugin_id, resource_type)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.is_empty(),
|
||||
Some(resource_type) => {
|
||||
let disabled_accounts = self
|
||||
.inner
|
||||
.store
|
||||
.disabled_plugin_accounts()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let resources = self
|
||||
.inner
|
||||
.state
|
||||
.resources(plugin_id, resource_type)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
resources.iter().any(|r| !disabled_accounts.contains(&r.id))
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
@@ -796,19 +826,54 @@ impl PluginRegistry {
|
||||
plugin_id: &str,
|
||||
resource_type: &str,
|
||||
) -> Result<ResourceRecord> {
|
||||
let disabled_accounts = self
|
||||
.inner
|
||||
.store
|
||||
.disabled_plugin_accounts()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let records = self.inner.state.resources(plugin_id, resource_type).await?;
|
||||
if records.is_empty() {
|
||||
let active_records: Vec<_> = records
|
||||
.into_iter()
|
||||
.filter(|record| !disabled_accounts.contains(&record.id))
|
||||
.collect();
|
||||
if active_records.is_empty() {
|
||||
return Err(Error::Provider(format!(
|
||||
"plugin '{plugin_id}' has no '{resource_type}' resource; add one first"
|
||||
"plugin '{plugin_id}' has no enabled '{resource_type}' resource; enable or add one first"
|
||||
)));
|
||||
}
|
||||
let now = now_ms();
|
||||
records
|
||||
.iter()
|
||||
.find(|record| record.state.is_ready(now))
|
||||
.or_else(|| records.first())
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::Provider("no plugin resource is available".into()))
|
||||
let mut ready_records: Vec<_> = active_records
|
||||
.into_iter()
|
||||
.filter(|record| record.state.is_ready(now))
|
||||
.collect();
|
||||
if ready_records.is_empty() {
|
||||
return Err(Error::Provider(format!(
|
||||
"all enabled accounts for plugin '{plugin_id}' are currently cooling or rate-limited"
|
||||
)));
|
||||
}
|
||||
|
||||
let get_priority = |r: &ResourceRecord| -> u8 {
|
||||
let label = r.private_data.get("quota").and_then(|q| q.get("planLabel")).and_then(|l| l.as_str()).unwrap_or("");
|
||||
let lower = label.to_lowercase();
|
||||
if label.contains("🔥") || lower.contains("pro") || lower.contains("ultra") || lower.contains("premium") || lower.contains("advanced") {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
ready_records.sort_by_key(|r| get_priority(r));
|
||||
if let Some(best_prio) = ready_records.first().map(|r| get_priority(r)) {
|
||||
ready_records.retain(|r| get_priority(r) == best_prio);
|
||||
}
|
||||
|
||||
let index = self
|
||||
.inner
|
||||
.rr_counter
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
% ready_records.len();
|
||||
Ok(ready_records[index].clone())
|
||||
}
|
||||
|
||||
async fn find_record(
|
||||
|
||||
@@ -102,7 +102,6 @@ export function buildChatBody(call: OpenAiChatCall): Record<string, JsonValue> {
|
||||
body.reasoning_effort = call.request.reasoning.effort;
|
||||
}
|
||||
if (call.request.latency === "fast") body.service_tier = "fast";
|
||||
if (call.request.cacheKey !== null) body.prompt_cache_key = call.request.cacheKey;
|
||||
return { ...body, ...call.extraBody };
|
||||
}
|
||||
|
||||
|
||||
@@ -425,9 +425,16 @@ impl HostContext {
|
||||
let raw_url = required_string(params, "url")?;
|
||||
let url = url::Url::parse(raw_url)
|
||||
.map_err(|error| Error::Config(format!("invalid plugin network URL: {error}")))?;
|
||||
if url.scheme() != "https" || !url.username().is_empty() || url.password().is_some() {
|
||||
let is_loopback = url
|
||||
.host_str()
|
||||
.map(|h| h == "127.0.0.1" || h == "localhost")
|
||||
.unwrap_or(false);
|
||||
if (url.scheme() != "https" && (!is_loopback || url.scheme() != "http"))
|
||||
|| !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
{
|
||||
return Err(Error::Config(
|
||||
"plugin network URL must be HTTPS without credentials".into(),
|
||||
"plugin network URL must be HTTPS without credentials (or loopback HTTP)".into(),
|
||||
));
|
||||
}
|
||||
let host = url
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Persists application settings.
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Result;
|
||||
@@ -10,6 +12,8 @@ const PROXY_SETTINGS_KEY: &str = "outbound_proxy";
|
||||
const TAB_SETTINGS_KEY: &str = "cursor_tab";
|
||||
const INSTALLATION_ID_KEY: &str = "installation_id";
|
||||
const DESKTOP_SETTINGS_KEY: &str = "desktop_lifecycle";
|
||||
const DISABLED_PLUGIN_MODELS_KEY: &str = "disabled_plugin_models";
|
||||
const DISABLED_PLUGIN_ACCOUNTS_KEY: &str = "disabled_plugin_accounts";
|
||||
|
||||
pub const PUBLIC_TAB_SERVICE_URL: &str = "https://tab.leokun.cn";
|
||||
|
||||
@@ -299,4 +303,56 @@ impl Store {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disabled_plugin_models(&self) -> Result<HashSet<String>> {
|
||||
let value = sqlx::query_scalar::<_, String>(
|
||||
"SELECT value_json FROM service_settings WHERE setting_key = ?",
|
||||
)
|
||||
.bind(DISABLED_PLUGIN_MODELS_KEY)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
value
|
||||
.map(|value| serde_json::from_str(&value).map_err(Into::into))
|
||||
.unwrap_or_else(|| Ok(HashSet::new()))
|
||||
}
|
||||
|
||||
pub async fn set_disabled_plugin_models(&self, model_ids: &HashSet<String>) -> Result<()> {
|
||||
let value_json = serde_json::to_string(model_ids)?;
|
||||
let _write = self.writes.lock().await;
|
||||
sqlx::query(
|
||||
"INSERT INTO service_settings(setting_key, value_json, updated_at_ms) VALUES (?, ?, ?) ON CONFLICT(setting_key) DO UPDATE SET value_json = excluded.value_json, updated_at_ms = excluded.updated_at_ms",
|
||||
)
|
||||
.bind(DISABLED_PLUGIN_MODELS_KEY)
|
||||
.bind(value_json)
|
||||
.bind(now_ms())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disabled_plugin_accounts(&self) -> Result<HashSet<String>> {
|
||||
let value = sqlx::query_scalar::<_, String>(
|
||||
"SELECT value_json FROM service_settings WHERE setting_key = ?",
|
||||
)
|
||||
.bind(DISABLED_PLUGIN_ACCOUNTS_KEY)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
value
|
||||
.map(|value| serde_json::from_str(&value).map_err(Into::into))
|
||||
.unwrap_or_else(|| Ok(HashSet::new()))
|
||||
}
|
||||
|
||||
pub async fn set_disabled_plugin_accounts(&self, account_ids: &HashSet<String>) -> Result<()> {
|
||||
let value_json = serde_json::to_string(account_ids)?;
|
||||
let _write = self.writes.lock().await;
|
||||
sqlx::query(
|
||||
"INSERT INTO service_settings(setting_key, value_json, updated_at_ms) VALUES (?, ?, ?) ON CONFLICT(setting_key) DO UPDATE SET value_json = excluded.value_json, updated_at_ms = excluded.updated_at_ms",
|
||||
)
|
||||
.bind(DISABLED_PLUGIN_ACCOUNTS_KEY)
|
||||
.bind(value_json)
|
||||
.bind(now_ms())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user