lazyBoy/crates/controld/src/main.rs

212 lines
6.5 KiB
Rust
Raw Normal View History

2026-09-07 14:23:38 +00:00
use std::sync::Arc;
2026-09-03 12:46:14 +00:00
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::routing::{get, post};
use axum::{Json, Router};
use lazyboy_control::{
2026-09-07 14:23:38 +00:00
ActionRequest, BrowserRequest, ComputerController, ComputerDriver, ControlContext,
ControlError, PRIMARY_DISPLAY, RecordingRequest, normalize_display,
observation_to_control_json,
2026-09-03 12:46:14 +00:00
};
#[derive(Clone)]
struct App {
token: String,
2026-09-07 14:23:38 +00:00
controller: Arc<dyn ComputerController>,
2026-09-03 12:46:14 +00:00
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let token = std::env::var("LAZYBOY_CONTROL_TOKEN").unwrap_or_default();
2026-09-07 14:23:38 +00:00
let driver = ComputerDriver::from_env();
tracing::info!(backend = driver.as_str(), "computer controller");
2026-09-03 12:46:14 +00:00
let app = Router::new()
.route("/health", get(|| async { "ok" }))
.route("/observe", post(observe))
.route("/act", post(act))
2026-09-07 14:23:38 +00:00
.route("/browser", post(browser))
.route("/recording/start", post(recording_start))
.route("/recording/stop", post(recording_stop))
.route("/recording/collect", post(recording_collect))
.with_state(App {
token,
controller: driver.controller(),
});
2026-09-03 12:46:14 +00:00
let listener = tokio::net::TcpListener::bind("127.0.0.1:7070")
.await
.expect("bind control port");
tracing::info!("controld listening on 127.0.0.1:7070");
axum::serve(listener, app).await.expect("serve");
}
fn authorized(headers: &HeaderMap, token: &str) -> bool {
if token.is_empty() {
return false;
}
headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == format!("Bearer {token}"))
}
fn header_value<'a>(headers: &'a HeaderMap, name: &'static str) -> Option<&'a str> {
2026-09-04 09:08:56 +00:00
headers
.get(name)
.and_then(|value| value.to_str().ok())
.filter(|value| !value.is_empty())
2026-09-03 12:46:14 +00:00
}
fn display_of(headers: &HeaderMap, fallback: Option<&str>) -> String {
normalize_display(
header_value(headers, "x-lazyboy-display")
.or(fallback)
.unwrap_or(PRIMARY_DISPLAY),
)
.to_string()
}
fn profile_of(headers: &HeaderMap, fallback: Option<&str>) -> Option<String> {
header_value(headers, "x-lazyboy-profile")
.or(fallback)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
2026-09-07 14:23:38 +00:00
fn status_for(error: &ControlError) -> StatusCode {
if error.is_client_error() {
StatusCode::BAD_REQUEST
} else {
tracing::error!(error = %error, "control failed");
StatusCode::INTERNAL_SERVER_ERROR
}
}
2026-09-04 09:08:56 +00:00
async fn observe(
State(app): State<App>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
2026-09-03 12:46:14 +00:00
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
}
2026-09-07 14:23:38 +00:00
let ctx = ControlContext::new(display_of(&headers, None), None);
match app.controller.observe(&ctx).await {
Ok(observation) => Ok(Json(observation_to_control_json(&observation))),
Err(error) => Err(status_for(&error)),
}
2026-09-03 12:46:14 +00:00
}
async fn act(
State(app): State<App>,
headers: HeaderMap,
Json(request): Json<ActionRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
}
2026-09-07 14:23:38 +00:00
let ctx = ControlContext::new(
display_of(&headers, request.display.as_deref()),
profile_of(&headers, request.profile_path.as_deref()),
);
match app.controller.act(&request, &ctx).await {
Ok(result) => {
let mut body = serde_json::json!({ "completed": result.completed });
if let Some(observation) = result.observation
&& let serde_json::Value::Object(map) = observation_to_control_json(&observation)
{
body.as_object_mut().expect("object").extend(map);
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
Ok(Json(body))
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
Err(error) => Err(status_for(&error)),
2026-09-03 12:46:14 +00:00
}
}
2026-09-07 14:23:38 +00:00
async fn browser(
State(app): State<App>,
headers: HeaderMap,
Json(request): Json<BrowserRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
}
let ctx = ControlContext::new(
display_of(&headers, request.display.as_deref()),
profile_of(&headers, request.profile_path.as_deref()),
);
match app.controller.browser(&request, &ctx).await {
Ok(page) => Ok(Json(
serde_json::to_value(&page).unwrap_or_else(|_| serde_json::json!({"ok": false})),
)),
Err(error) => Err(status_for(&error)),
}
}
2026-09-07 14:23:38 +00:00
fn recording_ctx(headers: &HeaderMap, request: &RecordingRequest) -> ControlContext {
ControlContext::new(
display_of(headers, request.display.as_deref()),
profile_of(headers, request.profile_path.as_deref()),
)
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
async fn recording_start(
State(app): State<App>,
headers: HeaderMap,
Json(request): Json<RecordingRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
match app
.controller
.start_recording(&request, &recording_ctx(&headers, &request))
.await
{
Ok(session) => Ok(Json(
serde_json::to_value(&session).unwrap_or_else(|_| serde_json::json!({"ok": false})),
)),
Err(error) => Err(status_for(&error)),
2026-09-04 09:08:56 +00:00
}
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
async fn recording_stop(
State(app): State<App>,
headers: HeaderMap,
Json(request): Json<RecordingRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
}
match app
.controller
.stop_recording(&request, &recording_ctx(&headers, &request))
.await
{
Ok(()) => Ok(Json(serde_json::json!({ "ok": true }))),
Err(error) => Err(status_for(&error)),
}
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
async fn recording_collect(
State(app): State<App>,
headers: HeaderMap,
Json(request): Json<RecordingRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !authorized(&headers, &app.token) {
return Err(StatusCode::UNAUTHORIZED);
2026-09-03 12:46:14 +00:00
}
2026-09-07 14:23:38 +00:00
match app
.controller
.collect_recording(&request, &recording_ctx(&headers, &request))
.await
{
Ok(result) => Ok(Json(
serde_json::to_value(&result).unwrap_or_else(|_| serde_json::json!({"events": []})),
)),
Err(error) => Err(status_for(&error)),
2026-09-03 12:46:14 +00:00
}
}