use std::{collections::HashMap, str::FromStr}; use mlua::{Error, Lua, LuaSerdeExt, Result, Table, Value}; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Method, }; use crate::utils::{net::get_request_user_agent_header, table_builder::ReadonlyTableBuilder}; pub async fn new(lua: &Lua) -> Result { ReadonlyTableBuilder::new(lua)? .with_function("jsonEncode", net_json_encode)? .with_function("jsonDecode", net_json_decode)? .with_async_function("request", net_request)? .build() } fn net_json_encode(_: &Lua, (val, pretty): (Value, Option)) -> Result { if let Some(true) = pretty { serde_json::to_string_pretty(&val).map_err(Error::external) } else { serde_json::to_string(&val).map_err(Error::external) } } fn net_json_decode(lua: &Lua, json: String) -> Result { let json: serde_json::Value = serde_json::from_str(&json).map_err(Error::external)?; lua.to_value(&json) } async fn net_request<'lua>(lua: &'lua Lua, config: Value<'lua>) -> Result> { // Extract stuff from config and make sure its all valid let (url, method, headers, body) = match config { Value::String(s) => { let url = s.to_string_lossy().to_string(); let method = "GET".to_string(); (url, method, None, None) } Value::Table(tab) => { // Extract url let url = match tab.raw_get::<&str, mlua::String>("url") { Ok(config_url) => config_url.to_string_lossy().to_string(), Err(_) => return Err(Error::RuntimeError("Missing 'url' in config".to_string())), }; // Extract method let method = match tab.raw_get::<&str, mlua::String>("method") { Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(), Err(_) => "GET".to_string(), }; // Extract headers let headers = match tab.raw_get::<&str, mlua::Table>("headers") { Ok(config_headers) => { let mut lua_headers = HeaderMap::new(); for pair in config_headers.pairs::() { let (key, value) = pair?; lua_headers.insert( HeaderName::from_str(key.to_str()?).map_err(Error::external)?, HeaderValue::from_str(value.to_str()?).map_err(Error::external)?, ); } Some(lua_headers) } Err(_) => None, }; // Extract body let body = match tab.raw_get::<&str, mlua::String>("body") { Ok(config_body) => Some(config_body.as_bytes().to_owned()), Err(_) => None, }; (url, method, headers, body) } _ => return Err(Error::RuntimeError("Invalid config value".to_string())), }; // Convert method string into proper enum let method = match Method::from_str(&method) { Ok(meth) => meth, Err(_) => { return Err(Error::RuntimeError(format!( "Invalid config method '{}'", &method ))) } }; // Extract headers from config, force user agent let mut header_map = if let Some(headers) = headers { headers } else { HeaderMap::new() }; header_map.insert( "User-Agent", HeaderValue::from_str(&get_request_user_agent_header()).map_err(Error::external)?, ); // Create a client to send a request with // FUTURE: Try to reuse this client let client = reqwest::Client::builder() .build() .map_err(Error::external)?; // Create and send the request let mut request = client.request(method, url).headers(header_map); if let Some(body) = body { request = request.body(body); } let response = request.send().await.map_err(Error::external)?; // Extract status, headers, body let res_status = response.status(); let res_headers = response.headers().clone(); let res_bytes = response.bytes().await.map_err(Error::external)?; // Construct and return a readonly lua table with results let tab = lua.create_table()?; tab.raw_set("ok", res_status.is_success())?; tab.raw_set("statusCode", res_status.as_u16())?; tab.raw_set( "statusMessage", res_status.canonical_reason().unwrap_or("?"), )?; tab.raw_set( "headers", res_headers .iter() .filter(|(_, value)| value.to_str().is_ok()) .map(|(key, value)| (key.as_str(), value.to_str().unwrap())) .collect::>(), )?; tab.raw_set("body", lua.create_string(&res_bytes)?)?; tab.set_readonly(true); Ok(Value::Table(tab)) }