1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

feat-webgarages-121 (#129)

### Description

Closes #121

### Please confirm

- [x] I am the legal owner or represent the legal owner of all work submitted (including LLM-generated code, if any)
- [x] I consent to my changes being added to this FOSS project
- [x] I have confirmed that this does not add new errors or warnings with `utils/clippy.sh`
- [ ] This PR used LLMs to generate some or all of the code changes

Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/129
This commit is contained in:
NG (Graham)
2026-06-15 01:02:52 +00:00
committed by NGnius
parent a222745841
commit d2626bb47f
40 changed files with 4012 additions and 282 deletions

View File

@@ -11,6 +11,7 @@ readme.workspace = true
actix-web = { workspace = true, features = [ "secure-cookies" ] }
actix-session = { version = "0.11", features = [ "cookie-session" ], default-features = false }
actix-identity = "0.9"
actix-multipart = "0.7"
cookie = "0.18"
actix-files.workspace = true
#actix-ws = "0.3"
@@ -21,8 +22,11 @@ tokio = { version = "1.43", features = [ "rt-multi-thread" ] }
clap.workspace = true
handlebars = { version = "6", features = ["dir_source"] }
oj_rc_core = { version = "*", path = "../rc_core" }
oj_rc_plugins = { version = "*", path = "../rc_plugins" }
oj_convert.workspace = true
libfj.workspace = true
git-version.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
chrono.workspace = true

View File

@@ -0,0 +1 @@
pub mod plugins;

View File

@@ -0,0 +1,124 @@
use oj_convert::{Bobocraft, BobocraftBlock};
pub struct RexLike {
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
}
impl RexLike {
pub fn new(convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>) -> Self {
Self {
convert,
}
}
}
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for RexLike {
fn file_ext(&self) -> &'static str {
"bobo"
}
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
let data = Bobocraft::parse(upload).map_err(|e| {
log::error!("Failed to parse bobocraft import: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let cubes = data.cubes.iter()
.map(|b| oj_rc_core::cubes::Cube {
id: b.cube_id,
x: b.x,
y: b.y,
z: b.z,
orientation: b.orientation,
}).collect();
let cube_data = oj_rc_core::cubes::Cube::dump_list(cubes)
.map_err(|e| {
log::error!("Failed to write cube data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let colours = data.cubes.iter()
.map(|b| oj_rc_core::cubes::Colour {
colour: b.color.unwrap_or(0),
x: b.x,
y: b.y,
z: b.z,
}).collect();
let colour_data = oj_rc_core::cubes::Colour::dump_list(colours)
.map_err(|e| {
log::error!("Failed to write colour data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let upgraded = self.convert.upgrade_to_modern(
&mut std::io::Cursor::new(&cube_data),
&mut std::io::Cursor::new(&colour_data),
).map_err(|e| {
log::warn!("Failed to upgrade bobo import data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
})?;
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
cube_data: upgraded.cube_data,
colour_data: upgraded.colour_data,
vehicle_name: Some(data.item_name),
vehicle_author: Some(data.added_by_display_name),
})
}
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
let cubes = oj_rc_core::cubes::Cube::parse_list(&mut std::io::Cursor::new(&data.cube_data))
.map_err(|e| {
log::error!("Failed to read cube data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let colours = oj_rc_core::cubes::Colour::parse_list(&mut std::io::Cursor::new(&data.colour_data))
.map_err(|e| {
log::error!("Failed to read colour data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
if colours.len() != cubes.len() {
log::error!("Failed to parse the same amount of cubes and colours (aborting)");
return Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid);
}
let mut placements = Vec::with_capacity(colours.len());
for i in 0..cubes.len() {
let colour = &colours[i];
let cube = &cubes[i];
// assumption: cube and colours are in the same order
let placement = BobocraftBlock {
cube_id: cube.id,
x: cube.x,
y: cube.y,
z: cube.z,
orientation: cube.orientation,
color: if (cube.x, cube.y, cube.z) == (colour.x, colour.y, colour.z) { Some(colour.colour) } else { None },
};
placements.push(placement);
}
let bobo = Bobocraft {
item_id: 0,
item_name: data.vehicle_name.clone().unwrap_or_else(|| "Exported vehicle".to_owned()),
item_description: format!("Exported by OpenJam {} {}", env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_VERSION")),
thumbnail: String::default(),
added_by: data.vehicle_author.clone().unwrap_or_default(),
added_by_display_name: data.vehicle_author.clone().unwrap_or_default(),
added_date: chrono::Utc::now().naive_utc(),
expiry_date: chrono::Utc::now().naive_utc(),
cpu: 0, // TODO?
total_robot_ranking: None, // TODO?
buyable: true,
buy_count: 0,
unknown_count: 0,
combat_rating: 5.0,
cosmetic_rating: 5.0,
featured: false,
banner_message: None,
offset: (0, 0, 0),
cubes: placements,
};
bobo.dump()
.map_err(|e| {
log::error!("Failed to write bobocraft format: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})
}
}
impl oj_rc_plugins::Plugin for RexLike {}

View File

@@ -0,0 +1,103 @@
use oj_convert::{Classic, ClassicBlock};
pub struct FifteenLike {
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
base_image: std::sync::Arc<Vec<u8>>,
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>,
}
impl FifteenLike {
pub fn new(
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
base_image: Vec<u8>,
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>,
) -> Self {
Self {
convert,
base_image: std::sync::Arc::new(base_image),
cpu_counter,
}
}
}
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for FifteenLike {
fn file_ext(&self) -> &'static str {
"png"
}
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
let data = Classic::parse(upload).map_err(|e| {
log::error!("Failed to parse RC15 import: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let cubes = data.cubes.iter()
.map(|b| oj_rc_core::cubes::Cube {
id: b.cube_id,
x: b.x,
y: b.y,
z: b.z,
orientation: b.orientation,
}).collect();
let cube_data = oj_rc_core::cubes::Cube::dump_list(cubes)
.map_err(|e| {
log::error!("Failed to write cube data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let converted = self.convert.convert_to_modern(
&mut std::io::Cursor::new(&cube_data),
).map_err(|e| {
log::warn!("Failed to modernize RC15 import data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
})?;
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
cube_data: converted.cube_data,
colour_data: converted.colour_data,
vehicle_name: Some(data.item_name),
vehicle_author: data.item_author,
})
}
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
let cpu_count = self.cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&data.cube_data)).total;
let classified = self.convert.convert_to_classic(
&mut std::io::Cursor::new(&data.cube_data),
&mut std::io::Cursor::new(&data.colour_data),
).map_err(|e| {
log::warn!("Failed to classic-ify RC15 export data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
})?;
let cubes = oj_rc_core::cubes::Cube::parse_list(&mut std::io::Cursor::new(&classified))
.map_err(|e| {
log::error!("Failed to read cube data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let mut placements = Vec::with_capacity(cubes.len());
for cube in cubes.iter() {
let placement = ClassicBlock {
cube_id: cube.id,
x: cube.x,
y: cube.y,
z: cube.z,
orientation: cube.orientation,
};
placements.push(placement);
}
let classic = Classic {
item_author: data.vehicle_author.clone(),
game_version: Some(1), // once told me
export_time: Some(chrono::Utc::now().to_rfc3339()),
item_name: data.vehicle_name.clone().unwrap_or_else(|| "Exported vehicle".to_owned()),
item_description: Some(format!("Exported by OpenJam {} {}", env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_VERSION"))),
item_tier: 1, // TODO
item_cpu: cpu_count,
cubes: placements,
};
classic.dump(&self.base_image)
.map_err(|e| {
log::error!("Failed to dump the RC15 PNG data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
})
}
}
impl oj_rc_plugins::Plugin for FifteenLike {}

View File

@@ -0,0 +1,102 @@
use std::io::Cursor;
use libfj::robocraft::{FactoryRobotGetInfo, FactoryInfo};
pub struct FactoryLike {
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
}
impl FactoryLike {
pub fn new(convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,) -> Self {
Self {
convert,
}
}
fn try_guess_json_format(upload: &[u8]) -> Option<FactoryRobotGetInfo> {
let reader = Cursor::new(upload);
match serde_json::from_reader::<_, FactoryRobotGetInfo>(reader) {
Ok(data) => Some(data),
Err(e) => {
let reader = Cursor::new(upload);
match serde_json::from_reader::<_, FactoryInfo<FactoryRobotGetInfo>>(reader) {
Ok(data) => Some(data.response),
Err(e2) => {
log::error!("Failed to parse json for factory-like vehicle import: (1){} (2){}", e, e2);
None
}
}
}
}
}
}
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for FactoryLike {
fn file_ext(&self) -> &'static str {
"rcbup"
}
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
if let Some(upload_data) = Self::try_guess_json_format(upload) {
use base64::Engine;
let cube_data = base64::engine::general_purpose::STANDARD.decode(&upload_data.cube_data).map_err(|e| {
log::error!("Bad base64 encoding of cube data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let colour_data = base64::engine::general_purpose::STANDARD.decode(&upload_data.cube_data).map_err(|e| {
log::error!("Bad base64 encoding of colour data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
let upgraded = self.convert.upgrade_to_modern(
&mut std::io::Cursor::new(&cube_data),
&mut std::io::Cursor::new(&colour_data),
).map_err(|e| {
log::warn!("Failed to upgrade rcbup import data: {}", e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
})?;
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
cube_data: upgraded.cube_data,
colour_data: upgraded.colour_data,
vehicle_name: Some(upload_data.item_name),
vehicle_author: Some(upload_data.added_by_display_name),
})
} else {
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid)
}
}
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
use base64::Engine;
let json_data = FactoryRobotGetInfo {
item_id: 0,
item_name: data.vehicle_name.clone().unwrap_or_default(),
item_description: String::default(),
thumbnail: String::default(),
added_by: data.vehicle_author.clone().unwrap_or_default(),
added_by_display_name: data.vehicle_author.clone().unwrap_or_default(),
added_date: "1970-01-01T00:00:01".to_owned(),
expiry_date: "2100-01-01T00:00:01".to_owned(),
rent_count: 0,
buy_count: 0,
cpu: 0, // TODO
total_robot_ranking: 0, // TODO
buyable: true,
removed_date: None,
ban_date: None,
featured: false,
banner_message: None,
combat_rating: 5.0,
cosmetic_rating: 5.0,
cube_data: base64::engine::general_purpose::STANDARD.encode(&data.cube_data),
colour_data: base64::engine::general_purpose::STANDARD.encode(&data.colour_data),
cube_amounts: String::default(),
};
let output = serde_json::to_vec_pretty(&json_data).map_err(|e| {
log::error!("Failed to convert vehicle `{:?}` to JSON: {}", json_data.item_name, e);
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
})?;
Ok(output)
}
}
impl oj_rc_plugins::Plugin for FactoryLike {}

View File

@@ -0,0 +1,61 @@
mod factory;
pub use factory::FactoryLike;
mod bobocraft;
pub use bobocraft::RexLike;
mod classic;
pub use classic::FifteenLike;
#[derive(Default)]
pub struct ImportPlugins {
map: std::collections::HashMap<String, Box<dyn oj_rc_plugins::vehicle_import::VehicleImportPlugin>>,
}
impl ImportPlugins {
pub fn standard(
assets_path: impl AsRef<std::path::Path>,
parsers: &oj_rc_core::cubes::CubeParsers,
) -> Self {
let mut map = std::collections::HashMap::with_capacity(3);
map.insert("rcbup".to_owned(), Box::new(FactoryLike::new(parsers.converter())) as _);
map.insert("bobo".to_owned(), Box::new(RexLike::new(parsers.converter())) as _);
let image_path = assets_path.as_ref().join("rc_export.png");
log::debug!("Loading PNG for RC15 export steganography from {}", image_path.display());
let image_data = std::fs::read(image_path).expect("Failed to read rc_export.png file required for exporting RC15 vehicles");
map.insert("rc15".to_owned(), Box::new(FifteenLike::new(parsers.converter(), image_data, parsers.cpu_counter())) as _);
Self {
map,
}
}
pub fn plugin_names(&self) -> impl std::iter::Iterator<Item=&'_ String> {
let mut to_sort = self.map.keys().collect::<Vec<_>>();
to_sort.sort_by_key(|x| x.to_lowercase());
to_sort.into_iter()
}
pub fn file_ext(&self, name: &str) -> Result<&'static str, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
if let Some(plugin) = self.map.get(name) {
Ok(plugin.file_ext())
} else {
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
}
}
pub fn import_by_name(&self, name: &str, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
if let Some(plugin) = self.map.get(name) {
plugin.import(upload)
} else {
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
}
}
pub fn export_by_name(&self, name: &str, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
if let Some(plugin) = self.map.get(name) {
plugin.export(data)
} else {
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
}
}
}

View File

@@ -0,0 +1 @@
pub mod garage;

View File

@@ -27,6 +27,14 @@ async fn main() -> std::io::Result<()> {
let server_settings = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::server_config(&config));
let parsers = oj_rc_core::cubes::CubeParsers::new(&config);
let vehicle_importers = crate::api::garage::plugins::ImportPlugins::standard(&cli_args.assets_robocraft, &parsers);
log::info!("Loaded {} vehicle import/export plugins", vehicle_importers.plugin_names().count());
let importers_ref = actix_web::web::Data::new(vehicle_importers);
let parsers_ref = actix_web::web::Data::new(parsers);
let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data");
let auth_ref = actix_web::web::Data::new(Box::new(users));
@@ -65,12 +73,22 @@ async fn main() -> std::io::Result<()> {
.app_data(handlebars_ref.clone())
.app_data(server_settings.clone())
.app_data(auth_ref.clone())
.app_data(importers_ref.clone())
.app_data(parsers_ref.clone())
.service(version_info)
.service(web::login::form_submit)
.service(web::login::form_load)
.service(web::favicon::favicon_standard)
.service(web::dashboard::get)
.service(web::dashboard::post) // for login redirect
.service(web::index::get)
.service(web::garage::list::get)
.service(web::garage::info::get)
.service(web::garage::export::get)
.service(web::garage::import::get_existing)
.service(web::garage::import::get_new)
.service(web::garage::import::post)
.service(web::garage::selected::get)
})
.bind((cli_args.ip, cli_args.port))?
.run()

View File

@@ -1,4 +1,4 @@
use actix_web::{get, web::Data, Responder, HttpRequest};
use actix_web::{get, post, web::Data, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
@@ -29,12 +29,12 @@ struct PermissionData {
banned: bool,
}
#[get("/dashboard")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
pub async fn dashboard_impl(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match super::try_auth_user(user_opt, auth.as_ref(), &req).await? {
super::LoginReturn::AuthFail(resp) => Ok(resp),
super::LoginReturn::Success(user) => {
// TODO
log::debug!("Rendering user's dashboard");
let creation_time = user.creation();
let creation_time_chrono = chrono::DateTime::<chrono::Utc>::from_timestamp_secs(creation_time).unwrap_or_default();
Ok(super::render_ok(
@@ -63,3 +63,13 @@ pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Bo
}
}
}
#[get("/dashboard")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
dashboard_impl(handlebars_ref, auth, user_opt, req).await
}
#[post("/dashboard")]
pub async fn post(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
dashboard_impl(handlebars_ref, auth, user_opt, req).await
}

View File

@@ -0,0 +1,52 @@
use actix_web::{HttpRequest, HttpResponse, Responder, get, http::StatusCode, web::{Data, Path, Query}};
use actix_identity::Identity;
use oj_rc_plugins::vehicle_import::VehicleImportData;
use crate::web::{LoginReturn, try_auth_user};
use crate::api::garage::plugins::ImportPlugins;
#[get("/garages/{id}/export")]
pub async fn get(id: Path<i32>, query: Query<super::PortQuery>, exporter: Data<ImportPlugins>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
match user.garage_by_id(*id).await {
Ok(Some(garage)) => {
let data = VehicleImportData {
cube_data: garage.robot_data,
colour_data: garage.colour_data,
vehicle_name: garage.name,
vehicle_author: Some(user.display_name().to_owned()),
};
match exporter.export_by_name(&query.plugin, &data) {
Ok(export) => {
//export.push(b'\n');
use actix_web::http::header::{ContentDisposition, TryIntoHeaderPair};
let ext = exporter.file_ext(&query.plugin).unwrap();
// TODO sanitise vehicle name and include it in the filename
let dispo = ContentDisposition::attachment(format!("export-{}-{}.{}", &query.plugin, *id, ext));
let (key, val) = dispo.try_into_pair().unwrap();
let mut resp = HttpResponse::with_body(StatusCode::OK, export);
resp.headers_mut().append(key, val);
Ok(resp.map_into_boxed_body())
},
Err(e) => {
log::error!("Failed export of vehicle {} for user {}: {}", id, user.public_id(), e);
Err(super::PluginPortError::from(e).into())
}
}
},
Ok(None) => {
Ok(HttpResponse::NotFound()
.finish())
},
Err(e) => {
log::error!("Failed to load garage ID for user {}: {}", user.public_id(), e);
Ok(HttpResponse::InternalServerError()
.finish())
}
}
}
}
}

View File

@@ -0,0 +1,221 @@
use actix_web::{get, post, web::{Data, Path}, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
use crate::web::{LoginReturn, try_auth_user, render_ok, render_err};
use crate::api::garage::plugins::ImportPlugins;
const FORM_NAME: &str = "garage_import";
#[derive(Serialize, Deserialize)]
struct RenderData {
display_name: String,
public_id: String,
garage: Option<GarageData>,
importers: Vec<String>,
selected_importer: Option<String>,
success: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct GarageData {
garage_id: i32,
slot: i32,
}
async fn import_impl(id: Option<i32>, handlebars_ref: Data<handlebars::Handlebars<'_>>, importer: Data<ImportPlugins>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let html = if let Some(id) = id {
match user.garage_by_id(id).await {
Ok(Some(garage)) => {
render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: Some(GarageData {
garage_id: id,
slot: garage.slot,
}),
importers: importer.plugin_names()
.map(|x| x.to_owned())
.collect(),
selected_importer: None,
success: None,
},
handlebars_ref.as_ref(),
FORM_NAME,
)
},
Ok(None) => {
log::error!("Failed to find vehicle {} for user {}", id, user.public_id());
render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: Some(GarageData {
garage_id: -1,
slot: -1,
}),
importers: Vec::new(),
selected_importer: None,
success: None,
},
"Vehicle not found".to_owned(),
handlebars_ref.as_ref(),
FORM_NAME,
)
},
Err(e) => {
log::error!("Failed to retrieve vehicle {} for user {}: {}", id, user.public_id(), e);
render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: Some(GarageData {
garage_id: -1,
slot: -1,
}),
importers: Vec::new(),
selected_importer: None,
success: None,
},
e.to_string(),
handlebars_ref.as_ref(),
FORM_NAME,
)
}
}
} else {
render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: None,
importers: importer.plugin_names()
.map(|x| x.to_owned())
.collect(),
selected_importer: None,
success: None,
},
handlebars_ref.as_ref(),
FORM_NAME,
)
};
Ok(
html
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}
#[get("/garages/{id}/import")]
pub async fn get_existing(id: Path<i32>, handlebars_ref: Data<handlebars::Handlebars<'_>>, importer: Data<ImportPlugins>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
import_impl(Some(*id), handlebars_ref, importer, auth, user_opt, req).await
}
#[get("/garages/import")]
pub async fn get_new(handlebars_ref: Data<handlebars::Handlebars<'_>>, importer: Data<ImportPlugins>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
import_impl(None, handlebars_ref, importer, auth, user_opt, req).await
}
#[derive(Debug, actix_multipart::form::MultipartForm)]
struct ImportForm {
//#[multipart]
files: Vec<actix_multipart::form::bytes::Bytes>,
garage_id: actix_multipart::form::text::Text<i32>,
plugin: actix_multipart::form::text::Text<String>,
}
#[post("/garages/import")]
pub async fn post(form: actix_multipart::form::MultipartForm<ImportForm>, handlebars_ref: Data<handlebars::Handlebars<'_>>, importer: Data<ImportPlugins>, parsers: Data<oj_rc_core::cubes::CubeParsers>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let total_size: usize = form.files.iter().map(|f| f.data.len()).sum();
if form.files.is_empty() {
return Err(super::PluginPortError {
code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid,
}.into());
}
if *form.garage_id != -1 && form.files.len() != 1 {
return Err(super::PluginPortError {
code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid,
}.into());
}
for (i, file) in form.files.iter().enumerate() {
#[cfg(debug_assertions)]
log::trace!("import {} file {} data: {:?}", &*form.plugin, i, &file.data[..]);
let import_data = match importer.import_by_name(&form.plugin, &file.data)
.map_err(|e| super::PluginPortError { code: e }) {
Ok(x) => x,
Err(e) => {
let html = render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: None,
importers: importer.plugin_names()
.map(|x| x.to_owned())
.collect(),
selected_importer: Some(form.plugin.0.clone()),
success: None,
},
format!("Failed to import file {}: {}", i, e),
handlebars_ref.as_ref(),
FORM_NAME,
);
return Ok(
html
.respond_to(&req)
.map_into_boxed_body()
);
},
};
let vehicle_data = oj_rc_core::persist::user::VehicleData {
name: import_data.vehicle_name,
slot: -1, // will be overriden in oj_rc_core
robot_data: import_data.cube_data,
colour_data: import_data.colour_data,
weapon_order: Vec::default(), // will be overriden in oj_rc_core
crf_id: None, // irrelevant
was_rated: None, // irrelevant
};
user.save_garage(
vehicle_data,
if *form.garage_id < 0 { None } else { Some(*form.garage_id) }, // multipart crate silliness
parsers.cpu_counter().as_ref(),
parsers.weapon_order().as_ref(),
).await?;
}
let html = render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: None,
importers: importer.plugin_names()
.map(|x| x.to_owned())
.collect(),
selected_importer: Some(form.plugin.0.clone()),
success: Some(format!(
"Imported {} vehicle{} ({:.2}KiB)",
form.files.len(),
if form.files.len() > 1 { "s" } else { "" },
total_size as f64 / 1024.0,
)),
},
handlebars_ref.as_ref(),
FORM_NAME,
);
Ok(
html
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}

View File

@@ -0,0 +1,93 @@
use actix_web::{get, web::{Data, Path}, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
use crate::web::{LoginReturn, try_auth_user, render_ok, render_err};
use crate::api::garage::plugins::ImportPlugins;
const FORM_NAME: &str = "garage_info";
#[derive(Serialize, Deserialize)]
struct RenderData {
display_name: String,
public_id: String,
garage: GarageData,
exporters: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct GarageData {
garage_id: i32,
slot: i32,
name: String,
}
#[get("/garages/{id}/info")]
pub async fn get(id: Path<i32>, handlebars_ref: Data<handlebars::Handlebars<'_>>, exporter: Data<ImportPlugins>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let html = match user.garage_by_id(*id).await {
Ok(Some(garage)) => {
render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: GarageData {
garage_id: *id,
slot: garage.slot,
name: garage.name.unwrap_or_default(),
},
exporters: exporter.plugin_names()
.map(|x| x.to_owned())
.collect(),
},
handlebars_ref.as_ref(),
FORM_NAME,
)
},
Ok(None) => {
log::error!("Failed to find vehicle {} for user {}", id, user.public_id());
render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: GarageData {
garage_id: -1,
slot: -1,
name: String::default(),
},
exporters: Vec::new(),
},
"Vehicle not found".to_owned(),
handlebars_ref.as_ref(),
FORM_NAME,
)
},
Err(e) => {
log::error!("Failed to retrieve vehicle {} for user {}: {}", id, user.public_id(), e);
render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garage: GarageData {
garage_id: -1,
slot: -1,
name: String::default(),
},
exporters: Vec::new(),
},
e.to_string(),
handlebars_ref.as_ref(),
FORM_NAME,
)
}
};
Ok(
html
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}

View File

@@ -0,0 +1,76 @@
use actix_web::{get, web::Data, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
use crate::web::{LoginReturn, try_auth_user, render_ok, render_err};
const FORM_NAME: &str = "garage_list";
#[derive(Serialize, Deserialize)]
struct RenderData {
display_name: String,
public_id: String,
garages: Vec<GarageData>,
}
#[derive(Serialize, Deserialize)]
struct GarageData {
garage_id: i32,
slot: i32,
name: String,
cpu: i32,
max_cpu: i32,
creation_time_unix: i64,
creation_time_iso: String,
}
#[get("/garages/list")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let html = match user.garages().await {
Ok(mut garage_infos) => {
garage_infos.sort_by_key(|g| g.slot);
render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garages: garage_infos.into_iter()
.map(|g| GarageData {
garage_id: g.id,
slot: g.slot,
name: g.name,
cpu: g.total_robot_cpu,
max_cpu: g.bay_cpu,
creation_time_unix: g.creation_time,
creation_time_iso: chrono::DateTime::<chrono::Utc>::from_timestamp_secs(g.creation_time).unwrap_or_default().to_rfc3339(),
})
.collect(),
},
handlebars_ref.as_ref(),
FORM_NAME,
)
},
Err(e) => {
log::error!("Failed to retrieve garages for user {}: {}", user.public_id(), e);
render_err(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
garages: Vec::new(),
},
e.to_string(),
handlebars_ref.as_ref(),
FORM_NAME,
)
}
};
Ok(
html
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}

View File

@@ -0,0 +1,37 @@
pub mod list;
pub mod export;
pub mod import;
pub mod info;
pub mod selected;
#[derive(serde::Deserialize)]
struct PortQuery {
pub plugin: String,
}
#[derive(Debug)]
struct PluginPortError {
code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode,
}
impl std::convert::From<oj_rc_plugins::vehicle_import::VehicleImportErrorCode> for PluginPortError {
fn from(value: oj_rc_plugins::vehicle_import::VehicleImportErrorCode) -> Self {
Self {
code: value,
}
}
}
impl core::fmt::Display for PluginPortError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code)
}
}
impl core::error::Error for PluginPortError {}
impl actix_web::error::ResponseError for PluginPortError {
fn status_code(&self) -> actix_web::http::StatusCode {
actix_web::http::StatusCode::UNPROCESSABLE_ENTITY
}
}

View File

@@ -0,0 +1,29 @@
use actix_web::{get, web::{Data, Redirect}, Responder, HttpRequest};
use actix_identity::Identity;
use crate::web::{LoginReturn, try_auth_user};
#[get("/garages/selected")]
pub async fn get(auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let resp = if let Some(selected_garage) = user.garage_id_selected().await? {
Redirect::to(format!("/garages/{}/info", selected_garage))
.respond_to(&req)
.map_into_boxed_body()
} else {
log::error!("User {} has no garage selected (bad database state?)", user.display_name());
Redirect::to("/garages/list".to_owned())
.respond_to(&req)
.map_into_boxed_body()
};
Ok(
resp
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}

View File

@@ -2,6 +2,7 @@ pub mod dashboard;
pub mod login;
pub mod favicon;
pub mod index;
pub mod garage;
use serde::Serialize;
use actix_web::{web::{Html, Redirect}, Responder};
@@ -51,19 +52,25 @@ async fn try_auth_user(user_opt: Option<actix_identity::Identity>, auth: &oj_rc_
if let Some(user) = user_opt {
let user_id = user.id()?;
match <oj_rc_core::UserImpl as oj_rc_core::UserProvider<()>>::web_authenticate(auth, user_id.clone()).await {
Ok(user) => Ok(LoginReturn::Success(user)),
Ok(user) => {
//log::debug!("Web auth success");
Ok(LoginReturn::Success(user))
},
Err(e) => {
log::warn!("Failed to login with token {}: {} ({:?})", user_id, e.message, e.code);
Ok(LoginReturn::AuthFail(
Redirect::to("/login")
.temporary()
.respond_to(req)
.map_into_boxed_body()
))
}
}
} else {
log::debug!("No user identity, redirecting to login page");
Ok(LoginReturn::AuthFail(
Redirect::to("/login")
.temporary()
.respond_to(req)
.map_into_boxed_body()
))