mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
add factory web frontend (#77)
### Description This PR adds a web frontend for factory. Please let me know if you think the directory names or API endpoints should be changed. For example, I personally think there are better names than rc_factory_api, and there may be better API names than webui... ### Game Robocraft ### Please confirm - [x] I am the legal owner or represent the owner of all work submitted - [x] I consent to my submission being added to this FOSS project - [x] This PR used LLMs to generate some or all of the code changes Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/77 Reviewed-by: NGnius <ngniusness@gmail.com> Co-authored-by: MaxSignal <kastera58@gmail.com> Co-committed-by: MaxSignal <kastera58@gmail.com>
This commit is contained in:
24
rc_factory_web/Cargo.toml
Normal file
24
rc_factory_web/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "oj_factory_web"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-web.workspace = true
|
||||
actix-files.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
git-version.workspace = true
|
||||
base64.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
handlebars = { version = "6", features = ["dir_source"] }
|
||||
|
||||
libfj.workspace = true
|
||||
oj_rc_core = { version = "*", path = "../rc_core" }
|
||||
oj_rc_factory = { version = "*", path = "../rc_factory" }
|
||||
3
rc_factory_web/run_debug.sh
Executable file
3
rc_factory_web/run_debug.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
RUST_LOG=debug cargo run
|
||||
23
rc_factory_web/src/cli.rs
Normal file
23
rc_factory_web/src/cli.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct CliArgs {
|
||||
/// TCP port on which to accept connections
|
||||
#[arg(short, long, default_value_t = 8012)]
|
||||
pub port: u16,
|
||||
|
||||
/// IP Address on which to accept connections
|
||||
#[arg(long, default_value_t = {"127.0.0.1".to_string()})]
|
||||
pub ip: String,
|
||||
|
||||
/// Assets root
|
||||
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
|
||||
pub assets: String,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
pub fn get() -> Self {
|
||||
Self::parse()
|
||||
}
|
||||
}
|
||||
63
rc_factory_web/src/main.rs
Normal file
63
rc_factory_web/src/main.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
mod cli;
|
||||
mod robocraft;
|
||||
|
||||
use actix_web::{App, HttpServer, Responder};
|
||||
use oj_rc_core::persist::config::{ConfigImpl, ConfigProvider};
|
||||
|
||||
#[actix_web::get("/version")]
|
||||
async fn index() -> impl Responder {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
|
||||
let authors = env!("CARGO_PKG_AUTHORS");
|
||||
let license = env!("CARGO_PKG_LICENSE");
|
||||
let repo = env!("CARGO_PKG_REPOSITORY");
|
||||
format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo)
|
||||
}
|
||||
|
||||
fn io_error(e: impl ToString) -> std::io::Error {
|
||||
std::io::Error::other(e.to_string())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
let cli_args = cli::CliArgs::get();
|
||||
|
||||
let conf = ConfigImpl::load(&cli_args.assets).map_err(io_error)?;
|
||||
let factory_enum = <ConfigImpl as ConfigProvider<()>>::factory(&conf).await.map_err(io_error)?;
|
||||
let factory_data = actix_web::web::Data::new(factory_enum);
|
||||
|
||||
let mut handlebars_conf = handlebars::Handlebars::new();
|
||||
let mut dir_conf = handlebars::DirectorySourceOptions::default();
|
||||
dir_conf.tpl_extension = ".html.hbs".to_owned();
|
||||
dir_conf.hidden = false;
|
||||
dir_conf.temporary = false;
|
||||
handlebars_conf
|
||||
.register_templates_directory(
|
||||
std::path::PathBuf::from(&cli_args.assets).parent().expect("Bad asset path").join("templates"),
|
||||
dir_conf,
|
||||
)
|
||||
.unwrap();
|
||||
let handlebars_ref = actix_web::web::Data::new(handlebars_conf);
|
||||
|
||||
let assets_root = actix_web::web::Data::new(std::path::PathBuf::from(&cli_args.assets));
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(factory_data.clone())
|
||||
.app_data(assets_root.clone())
|
||||
.app_data(handlebars_ref.clone())
|
||||
.service(index)
|
||||
.service(robocraft::web_ui::index)
|
||||
.service(robocraft::web_ui::app_js)
|
||||
.service(robocraft::web_ui::favicon)
|
||||
.service(robocraft::web_ui::favicon_standard)
|
||||
.service(robocraft::factory::crf_api::list)
|
||||
.service(robocraft::factory::crf_api::list_default)
|
||||
.service(robocraft::factory::crf_api::get)
|
||||
})
|
||||
.bind((cli_args.ip, cli_args.port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
145
rc_factory_web/src/robocraft/factory/crf_api.rs
Normal file
145
rc_factory_web/src/robocraft/factory/crf_api.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use actix_web::{get, post, web::{Data, Json, Path}, HttpResponse};
|
||||
use base64::Engine;
|
||||
|
||||
use libfj::robocraft::{
|
||||
FactoryInfo, RoboShopItemsInfo, FactoryRobotGetInfo, FactoryRobotListInfo,
|
||||
ListPayload, ListQuery, FactoryOrderType, FactoryTextSearchField,
|
||||
};
|
||||
use oj_rc_factory::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo};
|
||||
|
||||
fn parse_filter(filter: &str) -> Vec<u32> {
|
||||
if filter.trim().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
filter.split(',')
|
||||
.filter_map(|x| x.trim().parse::<u32>().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn payload_to_query(payload: &ListPayload) -> ListQuery {
|
||||
ListQuery {
|
||||
page: (payload.page.max(1)) as usize,
|
||||
page_size: (payload.page_size.clamp(1, 100)) as usize,
|
||||
order: FactoryOrderType::try_from(payload.order.clamp(0, u8::MAX.into()) as u8).unwrap_or(FactoryOrderType::Suggested),
|
||||
player_filter: payload.player_filter,
|
||||
movement_filter: parse_filter(&payload.movement_filter),
|
||||
movement_category_filter: parse_filter(&payload.movement_category_filter),
|
||||
weapon_filter: parse_filter(&payload.weapon_filter),
|
||||
weapon_category_filter: parse_filter(&payload.weapon_category_filter),
|
||||
minimum_cpu: if payload.minimum_cpu <= 0 { 0 } else { payload.minimum_cpu as usize },
|
||||
maximum_cpu: if payload.maximum_cpu <= 0 { usize::MAX } else { payload.maximum_cpu as usize },
|
||||
text_filter: payload.text_filter.clone(),
|
||||
text_search_field: FactoryTextSearchField::try_from(payload.text_search_field.clamp(0, u8::MAX.into()) as u8).unwrap_or(FactoryTextSearchField::All),
|
||||
buyable: payload.buyable,
|
||||
prepend_featured_robot: payload.prepend_featured_robot,
|
||||
featured_only: payload.featured_only,
|
||||
default_page: payload.default_page,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_info(vi: VehicleQueryInfo) -> FactoryRobotListInfo {
|
||||
FactoryRobotListInfo {
|
||||
item_id: vi.id as usize,
|
||||
item_name: vi.name,
|
||||
item_description: vi.description,
|
||||
thumbnail: vi.thumbnail,
|
||||
added_by: vi.added_by,
|
||||
added_by_display_name: vi.added_by_display_name,
|
||||
added_date: vi.added_date.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
||||
expiry_date: vi.expiry_date.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
||||
cpu: vi.cpu as usize,
|
||||
total_robot_ranking: vi.total_robot_ranking as usize,
|
||||
rent_count: vi.rent_count as usize,
|
||||
buy_count: vi.buy_count as usize,
|
||||
buyable: vi.buyable,
|
||||
removed_date: vi.removed_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()),
|
||||
ban_date: vi.ban_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()),
|
||||
featured: vi.featured,
|
||||
banner_message: vi.banner_message,
|
||||
combat_rating: vi.combat_rating as f32,
|
||||
cosmetic_rating: vi.cosmetic_rating as f32,
|
||||
cube_amounts: serde_json::to_string(&vi.cube_amounts).unwrap_or_else(|_| "{}".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_info(qi: VehicleQueryInfo, vi: VehicleInfo) -> FactoryRobotGetInfo {
|
||||
FactoryRobotGetInfo {
|
||||
item_id: qi.id as usize,
|
||||
item_name: qi.name,
|
||||
item_description: qi.description,
|
||||
thumbnail: qi.thumbnail,
|
||||
added_by: qi.added_by,
|
||||
added_by_display_name: qi.added_by_display_name,
|
||||
added_date: qi.added_date.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
||||
expiry_date: qi.expiry_date.format("%Y-%m-%dT%H:%M:%S").to_string(),
|
||||
cpu: qi.cpu as usize,
|
||||
total_robot_ranking: qi.total_robot_ranking as usize,
|
||||
rent_count: qi.rent_count as usize,
|
||||
buy_count: qi.buy_count as usize,
|
||||
buyable: qi.buyable,
|
||||
removed_date: qi.removed_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()),
|
||||
ban_date: qi.ban_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()),
|
||||
featured: qi.featured,
|
||||
banner_message: qi.banner_message,
|
||||
combat_rating: qi.combat_rating as f32,
|
||||
cosmetic_rating: qi.cosmetic_rating as f32,
|
||||
cube_data: base64::prelude::BASE64_STANDARD.encode(&vi.cube_data),
|
||||
colour_data: base64::prelude::BASE64_STANDARD.encode(&vi.colour_data),
|
||||
cube_amounts: serde_json::to_string(&qi.cube_amounts).unwrap_or_else(|_| "{}".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/roboShopItems/list")]
|
||||
pub async fn list(factory: Data<oj_rc_core::factory::Factory>, body: Json<ListPayload>) -> HttpResponse {
|
||||
if let oj_rc_core::factory::Factory::None = factory.get_ref() {
|
||||
return HttpResponse::ServiceUnavailable().body("factory adapter disabled");
|
||||
}
|
||||
|
||||
let query = payload_to_query(&body.into_inner());
|
||||
|
||||
match factory.list(query).await {
|
||||
Ok(items) => {
|
||||
let out: Vec<FactoryRobotListInfo> = items.into_iter().map(list_info).collect();
|
||||
HttpResponse::Ok().json(FactoryInfo {
|
||||
response: RoboShopItemsInfo { roboshop_items: out },
|
||||
status_code: 200,
|
||||
})
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("factory list error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/roboShopItems/list")]
|
||||
pub async fn list_default(factory: Data<oj_rc_core::factory::Factory>) -> HttpResponse {
|
||||
if let oj_rc_core::factory::Factory::None = factory.get_ref() {
|
||||
return HttpResponse::ServiceUnavailable().body("factory adapter disabled");
|
||||
}
|
||||
|
||||
let query = payload_to_query(&ListPayload::default());
|
||||
|
||||
match factory.list(query).await {
|
||||
Ok(items) => {
|
||||
let out: Vec<FactoryRobotListInfo> = items.into_iter().map(list_info).collect();
|
||||
HttpResponse::Ok().json(FactoryInfo {
|
||||
response: RoboShopItemsInfo { roboshop_items: out },
|
||||
status_code: 200,
|
||||
})
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("factory list error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/roboShopItems/get/{id}")]
|
||||
pub async fn get(factory: Data<oj_rc_core::factory::Factory>, id: Path<i32>) -> HttpResponse {
|
||||
if let oj_rc_core::factory::Factory::None = factory.get_ref() {
|
||||
return HttpResponse::ServiceUnavailable().body("factory adapter disabled");
|
||||
}
|
||||
match factory.vehicle(*id).await {
|
||||
Ok(Some((vehicle_info, query_info))) => HttpResponse::Ok().json(FactoryInfo {
|
||||
response: get_info(query_info, vehicle_info),
|
||||
status_code: 200,
|
||||
}),
|
||||
Ok(None) => HttpResponse::NotFound().body("robot not found"),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("factory get error: {e}")),
|
||||
}
|
||||
}
|
||||
1
rc_factory_web/src/robocraft/factory/mod.rs
Normal file
1
rc_factory_web/src/robocraft/factory/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod crf_api;
|
||||
2
rc_factory_web/src/robocraft/mod.rs
Normal file
2
rc_factory_web/src/robocraft/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod factory;
|
||||
pub mod web_ui;
|
||||
59
rc_factory_web/src/robocraft/web_ui.rs
Normal file
59
rc_factory_web/src/robocraft/web_ui.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use actix_web::{get, web::Data, HttpResponse};
|
||||
use handlebars::Handlebars;
|
||||
|
||||
const TEMPLATE_INDEX: &str = "rc_factory_web/index";
|
||||
const TEMPLATE_APP_JS: &str = "rc_factory_web/app.js";
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Context {
|
||||
version: String,
|
||||
source_url: String,
|
||||
}
|
||||
|
||||
fn version_string() -> String {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
//let license = env!("CARGO_PKG_LICENSE");
|
||||
//let repo = env!("CARGO_PKG_REPOSITORY");
|
||||
format!("OpenJam {} {}", name, version)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index(hb: Data<Handlebars<'_>>) -> HttpResponse {
|
||||
let ctx = Context {
|
||||
version: version_string(),
|
||||
source_url: env!("CARGO_PKG_REPOSITORY").to_string(),
|
||||
};
|
||||
|
||||
match hb.render(TEMPLATE_INDEX, &ctx) {
|
||||
Ok(body) => HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/html; charset=utf-8"))
|
||||
.body(body),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("template render error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/app.js")]
|
||||
pub async fn app_js(hb: Data<Handlebars<'_>>) -> HttpResponse {
|
||||
match hb.render(TEMPLATE_APP_JS, &()) {
|
||||
Ok(body) => HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "application/javascript; charset=utf-8"))
|
||||
.body(body),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("template render error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn favicon_impl(assets_root: Data<std::path::PathBuf>) -> impl actix_web::Responder {
|
||||
let path = assets_root.join("favicon.jpg");
|
||||
actix_files::NamedFile::open_async(path).await
|
||||
}
|
||||
|
||||
#[get("/robocraft/favicon")]
|
||||
pub async fn favicon(assets_root: Data<std::path::PathBuf>) -> impl actix_web::Responder {
|
||||
favicon_impl(assets_root).await
|
||||
}
|
||||
|
||||
#[get("/favicon.ico")]
|
||||
pub async fn favicon_standard(assets_root: Data<std::path::PathBuf>) -> impl actix_web::Responder {
|
||||
favicon_impl(assets_root).await
|
||||
}
|
||||
Reference in New Issue
Block a user