//
// A wrapper around https://git.estrogen.zone/dumbswitch.git to provide nice space api responses
// Copyright (C) 2025 memdmp
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
//
use std::env;
use std::io::Error;
use std::net::SocketAddr;
use std::sync::LazyLock;
use std::time::Duration;
use http_body_util::{BodyExt, Empty, Full};
use hyper::Uri;
use hyper::body::Incoming;
use hyper::header::HeaderValue;
use hyper::{Request, Response, body::Bytes, server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use serde_json::json;
use tokio::net::TcpStream;
use tokio::{net::TcpListener, sync::RwLock, task, time};
static RAW_JSON: &'static str = include_str!("../api.json");
static PARSED_JSON: LazyLock> =
LazyLock::new(|| RwLock::new(serde_json::from_str(RAW_JSON).expect("JSON parse error")));
type StdResult = std::result::Result;
type Result = StdResult>;
async fn respond(_: Request) -> Result>> {
let str = PARSED_JSON.read().await.to_string();
let len = str.len();
let mut res = Response::new(Full::new(Bytes::from(str)));
res
.headers_mut()
.append("Content-Type", HeaderValue::from_static("application/json"));
let len = HeaderValue::from_str(&len.to_string());
if len.is_ok() {
res.headers_mut().append("Content-Length", len.unwrap());
}
Ok(res)
}
async fn fetch_url(url: hyper::Uri) -> Result> {
if url.scheme_str() != Some("http") {
return Err(Box::new(Error::new(
std::io::ErrorKind::InvalidInput,
"This function only works with HTTP URIs.",
)));
}
let host = url.host().expect("uri has no host");
let port = url.port_u16().unwrap_or(80);
let addr = format!("{}:{}", host, port);
let stream = TcpStream::connect(addr).await?;
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(async move {
if let Err(err) = conn.await {
println!("Connection failed: {:?}", err);
}
});
let authority = url.authority().unwrap().clone();
let path = url.path();
let req = Request::builder()
.uri(path)
.header(hyper::header::HOST, authority.as_str())
.body(Empty::::new())?;
Ok(sender.send_request(req).await?)
}
fn get_request_target_uri() -> Uri {
env::var("REQUEST_TO")
.unwrap_or("http://10.0.0.77:8080/".to_string())
.parse::()
.unwrap()
}
async fn get_is_open() -> Result {
let mut res = fetch_url(get_request_target_uri()).await?;
let mut out = "".to_string();
while let Some(next) = res.frame().await {
let frame = next?;
if let Some(chunk) = frame.data_ref() {
out = format!("{out}{}", std::str::from_utf8(chunk).unwrap());
}
}
let state = out.trim().starts_with("a");
Ok(state)
}
#[tokio::main]
async fn main() -> Result<()> {
{
let state = {
let mut parsed_json = PARSED_JSON.write().await;
if !parsed_json.is_object() {
panic!("api.json must have top-level object!");
}
let parsed_json_obj = parsed_json.as_object_mut().unwrap();
let state = parsed_json_obj.get_mut("state");
let state = if state.is_some() {
state.unwrap()
} else {
&mut json!("{}")
};
let state_obj = state.as_object_mut().unwrap();
state_obj.remove_entry("open");
state.clone()
};
// ensure state is present
let mut parsed_json = PARSED_JSON.write().await;
let parsed_json_obj = parsed_json.as_object_mut().unwrap();
parsed_json_obj.insert("state".to_string(), state);
}
task::spawn(async {
loop {
{
let is_open = get_is_open().await;
let mut parsed_json = PARSED_JSON.write().await;
let state = parsed_json.get_mut("state");
if state.is_none() {
panic!("State is none!");
};
let state = state.unwrap().as_object_mut();
if state.is_none() {
panic!("State was turned into non-object!");
}
let state = state.unwrap();
if is_open.is_ok() {
let is_open = is_open.unwrap();
state.insert("open".to_string(), serde_json::Value::Bool(is_open));
} else {
state.remove("open");
eprintln!("Failed to fetch open status: {:#?}", is_open.unwrap_err());
}
}
time::sleep(Duration::from_secs(1)).await;
}
});
let addr: SocketAddr = env::var("LISTEN_ON")
.unwrap_or("127.0.0.1:3000".to_string())
.parse()?;
// We create a TcpListener and bind it to 127.0.0.1:3000
let listener = TcpListener::bind(addr).await?;
println!(
"Listening on: LISTEN_ON={addr:#?}
Sending fetch requests to: REQUEST_TO={:#?}",
get_request_target_uri()
);
// We start a loop to continuously accept incoming connections
loop {
let (stream, _) = listener.accept().await?;
// Use an adapter to access something implementing `tokio::io` traits as if they implement
// `hyper::rt` IO traits.
let io = TokioIo::new(stream);
// Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new()
// `service_fn` converts our function in a `Service`
.serve_connection(io, service_fn(respond))
.await
{
eprintln!("Error serving connection: {:?}", err);
}
});
}
}