feat: run http server to receive commands

This commit is contained in:
Ryan 2025-02-15 22:47:22 -05:00
parent a937db0be6
commit 5752e94ad2
Signed by: ErrorNoInternet
GPG Key ID: 2486BFB7B1E6A4A3
6 changed files with 138 additions and 7 deletions

10
Cargo.lock generated
View File

@ -1180,6 +1180,9 @@ dependencies = [
"azalea", "azalea",
"clap", "clap",
"futures", "futures",
"http-body-util",
"hyper",
"hyper-util",
"log", "log",
"mlua", "mlua",
"parking_lot", "parking_lot",
@ -1493,6 +1496,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.6.0" version = "1.6.0"
@ -1505,6 +1514,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",

View File

@ -20,6 +20,9 @@ anyhow = "1"
azalea = { git = "https://github.com/azalea-rs/azalea.git" } azalea = { git = "https://github.com/azalea-rs/azalea.git" }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
futures = "0" futures = "0"
http-body-util = "0"
hyper = { version = "1", features = ["server"] }
hyper-util = "0"
log = { version = "0" } log = { version = "0" }
mlua = { version = "0", features = ["async", "luau", "send"] } mlua = { version = "0", features = ["async", "luau", "send"] }
parking_lot = { version = "0" } parking_lot = { version = "0" }

View File

@ -1,5 +1,5 @@
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::{net::SocketAddr, path::PathBuf};
/// A Minecraft utility bot /// A Minecraft utility bot
#[derive(Parser)] #[derive(Parser)]
@ -7,4 +7,8 @@ pub struct Arguments {
/// Path to main Lua file /// Path to main Lua file
#[arg(short, long)] #[arg(short, long)]
pub script: Option<PathBuf>, pub script: Option<PathBuf>,
/// Socket address to bind HTTP server to
#[arg(short, long)]
pub address: Option<SocketAddr>,
} }

View File

@ -1,7 +1,10 @@
use crate::{State, commands::CommandSource, scripting}; use crate::{State, commands::CommandSource, http::handle, scripting};
use azalea::prelude::*; use azalea::prelude::*;
use log::info; use hyper::{server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use log::{error, info};
use mlua::Function; use mlua::Function;
use tokio::net::TcpListener;
pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> { pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> {
let globals = state.lua.lock().globals(); let globals = state.lua.lock().globals();
@ -40,7 +43,25 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow:
inner: Some(client), inner: Some(client),
}, },
)?; )?;
globals.get::<Function>("Init")?.call::<()>(())? globals.get::<Function>("Init")?.call::<()>(())?;
if let Some(address) = state.address {
let listener = TcpListener::bind(address).await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
let state = state.clone();
let service = service_fn(move |request| {
let state = state.clone();
async move { handle(request, state).await }
});
if let Err(error) = http1::Builder::new().serve_connection(io, service).await {
error!("failed to serve connection: {error:?}");
}
}
}
} }
_ => (), _ => (),
}; };

90
src/http.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::State;
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
use hyper::{Method, Request, Response, StatusCode, body::Bytes};
pub async fn handle(
request: Request<hyper::body::Incoming>,
state: State,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
let path = request.uri().path().to_owned();
match (request.method(), path.as_str()) {
(&Method::POST, "/reload") => {
let lua = state.lua.lock();
let config_path = match lua.globals().get::<String>("config_path") {
Ok(path) => path,
Err(error) => {
return Ok(status_code_response(
StatusCode::INTERNAL_SERVER_ERROR,
Some(full(format!(
"failed to get config_path from lua globals: {error:?}"
))),
));
}
};
if let Err(error) = match &std::fs::read_to_string(&config_path) {
Ok(string) => lua.load(string).exec(),
Err(error) => {
return Ok(status_code_response(
StatusCode::INTERNAL_SERVER_ERROR,
Some(full(format!("failed to read {config_path:?}: {error:?}"))),
));
}
} {
return Ok(status_code_response(
StatusCode::INTERNAL_SERVER_ERROR,
Some(full(format!(
"failed to execute configuration as lua code: {error:?}"
))),
));
}
Ok(Response::new(empty()))
}
(&Method::POST, "/eval" | "/exec") => {
let bytes = request.into_body().collect().await?.to_bytes();
let code = match std::str::from_utf8(&bytes) {
Ok(code) => code,
Err(error) => {
return Ok(status_code_response(
StatusCode::BAD_REQUEST,
Some(full(format!("invalid utf-8 data received: {error:?}"))),
));
}
};
let chunk = state.lua.lock().load(code);
Ok(Response::new(full(match path.as_str() {
"/eval" => format!("{:?}", chunk.eval::<String>()),
"/exec" => format!("{:?}", chunk.exec()),
_ => unreachable!(),
})))
}
(&Method::GET, "/ping") => Ok(Response::new(full("pong!"))),
_ => Ok(status_code_response(StatusCode::NOT_FOUND, None)),
}
}
fn status_code_response(
status_code: StatusCode,
bytes: Option<BoxBody<Bytes, hyper::Error>>,
) -> Response<BoxBody<Bytes, hyper::Error>> {
let mut response = Response::new(bytes.unwrap_or(empty()));
*response.status_mut() = status_code;
response
}
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
Full::new(chunk.into())
.map_err(|never| match never {})
.boxed()
}
fn empty() -> BoxBody<Bytes, hyper::Error> {
Empty::<Bytes>::new()
.map_err(|never| match never {})
.boxed()
}

View File

@ -3,6 +3,7 @@
mod arguments; mod arguments;
mod commands; mod commands;
mod events; mod events;
mod http;
mod scripting; mod scripting;
use azalea::{brigadier::prelude::CommandDispatcher, prelude::*}; use azalea::{brigadier::prelude::CommandDispatcher, prelude::*};
@ -11,12 +12,13 @@ use commands::{CommandSource, register};
use events::handle_event; use events::handle_event;
use mlua::Lua; use mlua::Lua;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{path::PathBuf, process::ExitCode, sync::Arc}; use std::{net::SocketAddr, path::PathBuf, process::ExitCode, sync::Arc};
#[derive(Default, Clone, Component)] #[derive(Default, Clone, Component)]
pub struct State { pub struct State {
lua: Arc<Mutex<Lua>>,
commands: Arc<CommandDispatcher<Mutex<CommandSource>>>, commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
lua: Arc<Mutex<Lua>>,
address: Option<SocketAddr>,
} }
#[tokio::main] #[tokio::main]
@ -73,8 +75,9 @@ async fn main() -> ExitCode {
let Err(error) = ClientBuilder::new() let Err(error) = ClientBuilder::new()
.set_handler(handle_event) .set_handler(handle_event)
.set_state(State { .set_state(State {
lua: Arc::new(Mutex::new(lua)),
commands: Arc::new(commands), commands: Arc::new(commands),
lua: Arc::new(Mutex::new(lua)),
address: args.address,
}) })
.start(account, server.as_ref()) .start(account, server.as_ref())
.await; .await;