mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-05 21:37:55 +00:00
319 lines
9 KiB
Rust
319 lines
9 KiB
Rust
use crate::{ApiContext, auth::AuthState, fail};
|
|
use axum::{
|
|
Extension,
|
|
extract::{Path, 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)]
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
struct ClusterStats {
|
|
pub guild_count: i32,
|
|
pub channel_count: i32,
|
|
}
|
|
|
|
#[api_endpoint]
|
|
pub async fn discord_state(State(ctx): State<ApiContext>) -> Json<Value> {
|
|
let mut shard_status = ctx
|
|
.redis
|
|
.hgetall::<HashMap<String, String>, &str>("pluralkit:shardstatus")
|
|
.await?
|
|
.values()
|
|
.map(|v| serde_json::from_str(v).expect("could not deserialize shard"))
|
|
.collect::<Vec<ShardState>>();
|
|
|
|
shard_status.sort_by(|a, b| b.shard_id.cmp(&a.shard_id));
|
|
|
|
Ok(Json(json!({
|
|
"shards": shard_status,
|
|
})))
|
|
}
|
|
|
|
#[api_endpoint]
|
|
pub async fn meta(State(ctx): State<ApiContext>) -> Json<Value> {
|
|
let stats = serde_json::from_str::<Value>(
|
|
ctx.redis
|
|
.get::<String, &'static str>("statsapi")
|
|
.await?
|
|
.as_str(),
|
|
)?;
|
|
|
|
Ok(Json(stats))
|
|
}
|
|
|
|
use std::time::Duration;
|
|
|
|
use crate::util::json_err;
|
|
use axum::{
|
|
extract,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use hyper::StatusCode;
|
|
use libpk::config;
|
|
use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel};
|
|
use reqwest::ClientBuilder;
|
|
|
|
#[derive(serde::Deserialize, Debug)]
|
|
pub struct CallbackRequestData {
|
|
redirect_domain: String,
|
|
code: String,
|
|
// state: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct CallbackDiscordData {
|
|
client_id: String,
|
|
client_secret: String,
|
|
grant_type: String,
|
|
redirect_uri: String,
|
|
code: String,
|
|
}
|
|
|
|
pub async fn discord_callback(
|
|
State(ctx): State<ApiContext>,
|
|
extract::Json(request_data): extract::Json<CallbackRequestData>,
|
|
) -> Response {
|
|
let client = ClientBuilder::new()
|
|
.connect_timeout(Duration::from_secs(3))
|
|
.timeout(Duration::from_secs(3))
|
|
.build()
|
|
.expect("error making client");
|
|
|
|
let reqbody = serde_urlencoded::to_string(&CallbackDiscordData {
|
|
client_id: config.discord().client_id.get().to_string(),
|
|
client_secret: config.discord().client_secret.clone(),
|
|
grant_type: "authorization_code".to_string(),
|
|
redirect_uri: request_data.redirect_domain, // change this!
|
|
code: request_data.code,
|
|
})
|
|
.expect("could not serialize");
|
|
|
|
let discord_resp = client
|
|
.post("https://discord.com/api/v10/oauth2/token")
|
|
.header("content-type", "application/x-www-form-urlencoded")
|
|
.body(reqbody)
|
|
.send()
|
|
.await
|
|
.expect("failed to request discord");
|
|
|
|
let Value::Object(discord_data) = discord_resp
|
|
.json::<Value>()
|
|
.await
|
|
.expect("failed to deserialize discord response as json")
|
|
else {
|
|
panic!("discord response is not an object")
|
|
};
|
|
|
|
if !discord_data.contains_key("access_token") {
|
|
return json_err(
|
|
StatusCode::BAD_REQUEST,
|
|
format!(
|
|
"{{\"error\":\"{}\"\"}}",
|
|
discord_data
|
|
.get("error_description")
|
|
.expect("missing error_description from discord")
|
|
.to_string()
|
|
),
|
|
);
|
|
};
|
|
|
|
let token = format!(
|
|
"Bearer {}",
|
|
discord_data
|
|
.get("access_token")
|
|
.expect("missing access_token")
|
|
.as_str()
|
|
.unwrap()
|
|
);
|
|
|
|
let discord_client = twilight_http::Client::new(token);
|
|
|
|
let user = discord_client
|
|
.current_user()
|
|
.await
|
|
.expect("failed to get current user from discord")
|
|
.model()
|
|
.await
|
|
.expect("failed to parse user model from discord");
|
|
|
|
let system: Option<PKSystem> = sqlx::query_as(
|
|
r#"
|
|
select systems.*
|
|
from accounts
|
|
left join systems on accounts.system = systems.id
|
|
where accounts.uid = $1
|
|
"#,
|
|
)
|
|
.bind(user.id.get() as i64)
|
|
.fetch_optional(&ctx.db)
|
|
.await
|
|
.expect("failed to query");
|
|
|
|
let Some(system) = system else {
|
|
return json_err(
|
|
StatusCode::BAD_REQUEST,
|
|
"user does not have a system registered".to_string(),
|
|
);
|
|
};
|
|
|
|
let system_config: Option<PKSystemConfig> = sqlx::query_as(
|
|
r#"
|
|
select * from system_config where system = $1
|
|
"#,
|
|
)
|
|
.bind(system.id)
|
|
.fetch_optional(&ctx.db)
|
|
.await
|
|
.expect("failed to query");
|
|
|
|
let system_config = system_config.unwrap();
|
|
|
|
// create dashboard token for system
|
|
|
|
let token = system.clone().token;
|
|
|
|
(
|
|
StatusCode::OK,
|
|
serde_json::to_string(&serde_json::json!({
|
|
"system": system.to_json(PrivacyLevel::Private),
|
|
"config": system_config.to_json(),
|
|
"user": user,
|
|
"token": token,
|
|
}))
|
|
.expect("should not error"),
|
|
)
|
|
.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"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[api_endpoint]
|
|
pub async fn dash_view(State(ctx): State<ApiContext>, Path(id): Path<String>) -> Json<Value> {
|
|
match sqlx::query_as::<Postgres, PKDashView>("select * from dash_views where id = $1")
|
|
.bind(id)
|
|
.fetch_optional(&ctx.db)
|
|
.await
|
|
{
|
|
Ok(val) => {
|
|
let Some(val) = val else {
|
|
return Err(crate::error::GENERIC_BAD_REQUEST);
|
|
};
|
|
Ok(Json(val.to_json()))
|
|
}
|
|
Err(err) => fail!(?err, "failed to query dash views"),
|
|
}
|
|
}
|