most of a dash views api impl

This commit is contained in:
alyssa 2025-12-21 17:24:04 -05:00
parent 832f07675f
commit 2e3390b27c
5 changed files with 176 additions and 5 deletions

View file

@ -1,10 +1,11 @@
use crate::ApiContext;
use axum::{extract::State, response::Json};
use crate::{ApiContext, auth::AuthState, error::fail};
use axum::{Extension, extract::State, response::Json};
use fred::interfaces::*;
use libpk::state::ShardState;
use pk_macros::api_endpoint;
use serde::Deserialize;
use serde_json::{Value, json};
use sqlx::Postgres;
use std::collections::HashMap;
#[allow(dead_code)]
@ -53,7 +54,7 @@ use axum::{
};
use hyper::StatusCode;
use libpk::config;
use pluralkit_models::{PKSystem, PKSystemConfig, PrivacyLevel};
use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel};
use reqwest::ClientBuilder;
#[derive(serde::Deserialize, Debug)]
@ -187,3 +188,109 @@ pub async fn discord_callback(
)
.into_response()
}
#[derive(serde::Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum DashViewRequest {
Add {
name: String,
value: String,
},
Patch {
id: String,
name: Option<String>,
value: Option<String>,
},
Remove { id: String },
}
#[api_endpoint]
pub async fn dash_views(
Extension(auth): Extension<AuthState>,
State(ctx): State<ApiContext>,
extract::Json(body): extract::Json<DashViewRequest>,
) -> Json<Value> {
let Some(system_id) = auth.system_id() else {
return Err(crate::error::GENERIC_AUTH_ERROR);
};
match body {
DashViewRequest::Add { name, value } => {
match sqlx::query_as::<Postgres, PKDashView>(
"select * from dash_views where name = $1 and system = $2",
)
.bind(&name)
.bind(system_id)
.fetch_optional(&ctx.db)
.await
{
Ok(val) => {
if val.is_some() {
return Err(crate::error::GENERIC_BAD_REQUEST);
};
match sqlx::query_as::<Postgres, PKDashView>(
"insert into dash_views (system, name, value) values ($1, $2, $3) returning *",
)
.bind(system_id)
.bind(name)
.bind(value)
.fetch_one(&ctx.db)
.await
{
Ok(res) => Ok(Json(res.to_json())),
Err(err) => fail!(?err, "failed to insert dash views"),
}
}
Err(err) => fail!(?err, "failed to query dash views"),
}
}
DashViewRequest::Patch { id, name, value } => {
match sqlx::query_as::<Postgres, PKDashView>(
"select * from dash_views where id = $1 and system = $2",
)
.bind(id)
.bind(system_id)
.fetch_optional(&ctx.db)
.await
{
Ok(val) => {
let Some(val) = val else {
return Err(crate::error::GENERIC_BAD_REQUEST);
};
// update
Ok(Json(Value::Null))
}
Err(err) => fail!(?err, "failed to query dash views"),
}
}
DashViewRequest::Remove { id } => {
match sqlx::query_as::<Postgres, PKDashView>(
"select * from dash_views where id = $1 and system = $2",
)
.bind(id)
.bind(system_id)
.fetch_optional(&ctx.db)
.await
{
Ok(val) => {
let Some(val) = val else {
return Err(crate::error::GENERIC_BAD_REQUEST);
};
match sqlx::query::<Postgres>(
"delete from dash_views where id = $1 and system = $2 returning *",
)
.bind(val.id)
.bind(system_id)
.fetch_one(&ctx.db)
.await
{
Ok(_) => Ok(Json(Value::Null)),
Err(err) => fail!(?err, "failed to remove dash views"),
}
}
Err(err) => fail!(?err, "failed to query dash views"),
}
}
}
}

View file

@ -3,7 +3,7 @@ use pk_macros::api_endpoint;
use serde_json::{Value, json};
use sqlx::Postgres;
use pluralkit_models::{PKSystem, PKSystemConfig, PrivacyLevel};
use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel};
use crate::{ApiContext, auth::AuthState, error::fail};
@ -36,7 +36,32 @@ pub async fn get_system_settings(
}
Ok(Json(match access_level {
PrivacyLevel::Private => config.to_json(),
PrivacyLevel::Private => {
let mut config_json = config.clone().to_json();
match sqlx::query_as::<Postgres, PKDashView>(
"select * from dash_views where system = $1",
)
.bind(system.id)
.fetch_all(&ctx.db)
.await
{
Ok(val) => {
config_json.as_object_mut().unwrap().insert(
"dash_views".to_string(),
serde_json::to_value(
&val.iter()
.map(|v| v.clone().to_json())
.collect::<Vec<serde_json::Value>>(),
)
.unwrap(),
);
}
Err(err) => fail!(?err, "failed to query dash views"),
};
config_json
}
PrivacyLevel::Public => json!({
"pings_enabled": config.pings_enabled,
"latch_timeout": config.latch_timeout,

View file

@ -123,6 +123,7 @@ fn router(ctx: ApiContext) -> Router {
.route("/private/discord/callback", post(rproxy))
.route("/private/discord/callback2", post(endpoints::private::discord_callback))
.route("/private/discord/shard_state", get(endpoints::private::discord_state))
.route("/private/dash_views", post(endpoints::private::dash_views))
.route("/private/stats", get(endpoints::private::meta))
.route("/v2/systems/{system_id}/oembed.json", get(rproxy))

View file

@ -0,0 +1,27 @@
-- database version 55
-- dashboard views
create function generate_dash_view_id_inner() returns char(10) as $$
select string_agg(substr('aieu234567890', ceil(random() * 13)::integer, 1), '') from generate_series(1, 10)
$$ language sql volatile;
create function generate_dash_view_id() returns char(10) as $$
declare newid char(10);
begin
loop
newid := generate_dash_view_id_inner();
if not exists (select 1 from dash_views where id = newid) then return newid; end if;
end loop;
end
$$ language plpgsql volatile;
create table dash_views (
id text not null primary key default generate_dash_view_id(),
system int references systems(id) on delete cascade,
name text not null,
value text not null,
unique (system, name)
);
update info set schema_version = 55;

View file

@ -93,3 +93,14 @@ struct SystemConfig {
#[json = "premium_lifetime"]
premium_lifetime: bool
}
#[pk_model]
struct DashView {
#[json = "id"]
id: String,
system: SystemId,
#[json = "name"]
name: String,
#[json = "value"]
value: String
}