pinakes/crates/pinakes-server/src/routes/plugins.rs
NotAShelf df1c46fa5c
treewide: cleanup
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia01590cdeed872cc8ebd16f6ca95f3cc6a6a6964
2026-03-11 21:30:58 +03:00

227 lines
6.4 KiB
Rust

use std::{collections::HashMap, sync::Arc};
use axum::{
Json,
extract::{Path, State},
};
use pinakes_core::plugin::PluginManager;
use crate::{
dto::{
InstallPluginRequest,
PluginEventRequest,
PluginResponse,
PluginUiPageEntry,
PluginUiWidgetEntry,
TogglePluginRequest,
},
error::ApiError,
state::AppState,
};
fn require_plugin_manager(
state: &AppState,
) -> Result<Arc<PluginManager>, ApiError> {
state.plugin_manager.clone().ok_or_else(|| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
"Plugin system is not enabled".to_string(),
))
})
}
/// List all installed plugins
pub async fn list_plugins(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginResponse>>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
let plugins = plugin_manager.list_plugins().await;
let mut responses = Vec::with_capacity(plugins.len());
for meta in plugins {
let enabled = plugin_manager.is_plugin_enabled(&meta.id).await;
responses.push(PluginResponse::new(meta, enabled));
}
Ok(Json(responses))
}
/// Get a specific plugin by ID
pub async fn get_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<PluginResponse>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| {
ApiError(pinakes_core::error::PinakesError::NotFound(format!(
"Plugin not found: {id}"
)))
})?;
let enabled = plugin_manager.is_plugin_enabled(&id).await;
Ok(Json(PluginResponse::new(plugin, enabled)))
}
/// Install a plugin from URL or file path
pub async fn install_plugin(
State(state): State<AppState>,
Json(req): Json<InstallPluginRequest>,
) -> Result<Json<PluginResponse>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
let plugin_id =
plugin_manager
.install_plugin(&req.source)
.await
.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to install plugin: {e}"),
))
})?;
let plugin =
plugin_manager.get_plugin(&plugin_id).await.ok_or_else(|| {
ApiError(pinakes_core::error::PinakesError::NotFound(
"Plugin installed but not found".to_string(),
))
})?;
let enabled = plugin_manager.is_plugin_enabled(&plugin_id).await;
Ok(Json(PluginResponse::new(plugin, enabled)))
}
/// Uninstall a plugin
pub async fn uninstall_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
plugin_manager.uninstall_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to uninstall plugin: {e}"),
))
})?;
Ok(Json(serde_json::json!({"uninstalled": true})))
}
/// Enable or disable a plugin
pub async fn toggle_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
Json(req): Json<TogglePluginRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
if req.enabled {
plugin_manager.enable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to enable plugin: {e}"),
))
})?;
} else {
plugin_manager.disable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to disable plugin: {e}"),
))
})?;
}
// Re-discover capabilities after toggle so cached data stays current
if let Some(ref pipeline) = state.plugin_pipeline
&& let Err(e) = pipeline.discover_capabilities().await
{
tracing::warn!(
plugin_id = %id,
error = %e,
"failed to re-discover capabilities after plugin toggle"
);
}
Ok(Json(serde_json::json!({
"id": id,
"enabled": req.enabled
})))
}
/// List all UI pages provided by loaded plugins
pub async fn list_plugin_ui_pages(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginUiPageEntry>>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
let pages = plugin_manager.list_ui_pages_with_endpoints().await;
let entries = pages
.into_iter()
.map(|(plugin_id, page, allowed_endpoints)| {
PluginUiPageEntry {
plugin_id,
page,
allowed_endpoints,
}
})
.collect();
Ok(Json(entries))
}
/// List all UI widgets provided by loaded plugins
pub async fn list_plugin_ui_widgets(
State(state): State<AppState>,
) -> Result<Json<Vec<PluginUiWidgetEntry>>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
let widgets = plugin_manager.list_ui_widgets().await;
let entries = widgets
.into_iter()
.map(|(plugin_id, widget)| PluginUiWidgetEntry { plugin_id, widget })
.collect();
Ok(Json(entries))
}
/// Receive a plugin event emitted from the UI and dispatch it to interested
/// server-side event-handler plugins via the pipeline.
pub async fn emit_plugin_event(
State(state): State<AppState>,
Json(req): Json<PluginEventRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
tracing::info!(event = %req.event, "plugin UI event received");
state.emit_plugin_event(&req.event, &req.payload);
Ok(Json(
serde_json::json!({ "received": true, "event": req.event }),
))
}
/// List merged CSS custom property overrides from all enabled plugins
pub async fn list_plugin_ui_theme_extensions(
State(state): State<AppState>,
) -> Result<Json<HashMap<String, String>>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
Ok(Json(plugin_manager.list_ui_theme_extensions().await))
}
/// Reload a plugin (for development)
pub async fn reload_plugin(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
let plugin_manager = require_plugin_manager(&state)?;
plugin_manager.reload_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to reload plugin: {e}"),
))
})?;
// Re-discover capabilities after reload so cached data stays current
if let Some(ref pipeline) = state.plugin_pipeline
&& let Err(e) = pipeline.discover_capabilities().await
{
tracing::warn!(
plugin_id = %id,
error = %e,
"failed to re-discover capabilities after plugin reload"
);
}
Ok(Json(serde_json::json!({"reloaded": true})))
}