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

234
Cargo.lock generated
View File

@@ -31,7 +31,7 @@ dependencies = [
"actix-web",
"bitflags",
"bytes",
"derive_more",
"derive_more 2.1.1",
"futures-core",
"http-range",
"log",
@@ -57,7 +57,7 @@ dependencies = [
"brotli",
"bytes",
"bytestring",
"derive_more",
"derive_more 2.1.1",
"encoding_rs",
"flate2",
"foldhash",
@@ -90,7 +90,7 @@ dependencies = [
"actix-session",
"actix-utils",
"actix-web",
"derive_more",
"derive_more 2.1.1",
"futures-core",
"serde",
"tracing",
@@ -106,6 +106,44 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "actix-multipart"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"derive_more 0.99.20",
"futures-core",
"futures-util",
"httparse",
"local-waker",
"log",
"memchr",
"mime",
"rand 0.8.6",
"serde",
"serde_json",
"serde_plain",
"tempfile",
"tokio",
]
[[package]]
name = "actix-multipart-derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b"
dependencies = [
"darling",
"parse-size",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "actix-router"
version = "0.5.4"
@@ -167,7 +205,7 @@ dependencies = [
"actix-utils",
"actix-web",
"anyhow",
"derive_more",
"derive_more 2.1.1",
"rand 0.9.4",
"serde",
"serde_json",
@@ -203,7 +241,7 @@ dependencies = [
"bytestring",
"cfg-if",
"cookie 0.16.2",
"derive_more",
"derive_more 2.1.1",
"encoding_rs",
"foldhash",
"futures-core",
@@ -520,6 +558,15 @@ dependencies = [
"num-traits 0.2.19",
]
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -915,6 +962,15 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
@@ -958,6 +1014,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.10.0"
@@ -1059,6 +1121,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@@ -1256,6 +1324,19 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "derive_more"
version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.117",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1271,7 +1352,7 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
@@ -1395,6 +1476,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -1465,6 +1558,21 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "ff"
version = "0.13.1"
@@ -1786,6 +1894,15 @@ dependencies = [
"walkdir",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1821,6 +1938,20 @@ dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -2457,6 +2588,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -2831,6 +2968,18 @@ dependencies = [
"zip",
]
[[package]]
name = "oj_convert"
version = "0.3.0"
dependencies = [
"bytes",
"chrono",
"png",
"postcard",
"serde",
"serde_json",
]
[[package]]
name = "oj_polariton_auth"
version = "1.3.0"
@@ -3163,8 +3312,10 @@ version = "1.3.0"
dependencies = [
"actix-files",
"actix-identity",
"actix-multipart",
"actix-session",
"actix-web",
"base64 0.22.1",
"chrono",
"clap",
"cookie 0.18.1",
@@ -3173,7 +3324,9 @@ dependencies = [
"handlebars",
"libfj",
"log",
"oj_convert",
"oj_rc_core",
"oj_rc_plugins",
"serde",
"serde_json",
"tokio",
@@ -3299,6 +3452,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "password-hash"
version = "0.5.0"
@@ -3436,6 +3595,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "polariton"
version = "0.6.0"
@@ -3484,6 +3656,19 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "postcard"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless",
"serde",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -4097,6 +4282,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.40"
@@ -4233,7 +4431,7 @@ dependencies = [
"async-trait",
"bigdecimal",
"chrono",
"derive_more",
"derive_more 2.1.1",
"futures-util",
"log",
"mac_address",
@@ -4473,6 +4671,15 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -4977,6 +5184,19 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.18"

View File

@@ -49,3 +49,5 @@ hex = "0.4"
base64 = "0.22"
#oj_serdes = { version = "0.3.0", path = "../oj_core/serdes" }
oj_serdes = "0.3.0"
oj_convert = { version = "0.3.0", path = "../oj_core/convert" }
#oj_convert = "0.3.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -11,144 +11,19 @@
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Dashboard - Openjam" />
<meta property="og:description" content="Create an account for third-party Robocraft servers" />
<meta property="og:description" content="View your account for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Dashboard - Openjam" />
<meta property="twitter:description" content="Create an account for third-party Robocraft servers" />
<meta property="twitter:description" content="View your account for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 90%;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 0 0 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-header-right {
position: relative;
width: 25%;
text-align: right;
margin: 0 0 0 auto;
box-sizing: border-box;
padding: 0 25px;
}
.form-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: dimgrey;
color: black;
padding: 25px;
}
.form-padded {
padding: 0 25px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
a { text-decoration-line: none; }
a:link { color: darkgrey; }
a:hover { color: white; }
a:visited { color: lightgrey; }
a:active { color: white; }
form {
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 250px;
}
.form-error {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-text-block {
width: 80%;
margin: 0 auto;
}
.access-grid {
width: 80%;
display: grid;
margin: 0 auto;
padding: 5px;
grid-template-columns: auto auto auto auto;
gap: 5px;
}
.access-box {
background-color: dimgrey;
color: black;
padding: 5px 0;
text-align: center;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
}
.access-box-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: black;
color: lightgrey;
padding: 5px;
}
.box-details {
text-align: left;
display: grid;
grid-template-columns: auto auto;
gap: 2px;
padding: 5px;
}
.box-details-key {
padding: 0 5px;
background-color: black;
color: lightgrey;
text-align: left;
}
.box-details-value {
padding: 0 5px;
background-color: lightgrey;
color: black;
text-align: left;
}
footer {
position: relative;
width: 100%
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">
@@ -200,7 +75,7 @@
<div class="box-details-key">Total</div> <!-- TODO -->
<div class="box-details-value">{{form.debug.user_id}}/{{form.debug.user_id}}</div>
</div>
<a href="#"> <!-- TODO -->
<a href="/garages/list">
<div class="access-box-banner">
View My Garages
</div>

View File

@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Garages - Openjam</title>
<meta name="title" content="Garage{{#if form.garage}} #{{form.garage.slot}}{{/if}} Import - Openjam" />
<meta name="description" content="User garage import for third-party Robocraft servers" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Garage Import - Openjam" />
<meta property="og:description" content="Import a vehicle into your garage for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Garage Import - Openjam" />
<meta property="twitter:description" content="Import a vehicle into your garages for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
Welcome {{form.display_name}}! |
<a href="/">Home</a>
</div>
<h1 class="form-banner">{{form.display_name}} Garage {{#if form.garage}}#{{form.garage.slot}}{{/if}} Import</h1>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
{{#if form.success}}
<div class="form-success">
<p class="form-success-message">{{form.success}}</p>
</div>
{{/if}}
<form method="post" enctype="multipart/form-data" action="/garages/import">
<div class="access-list">
{{#if form.garage}}
<input type="hidden" id="garage_id" name="garage_id" value="{{form.garage.garage_id}}"/>
{{else}}
<input type="hidden" id="garage_id" name="garage_id" value="-1"/>
{{/if}}
<div class="access-row-box">
<label>Import with</label>
<select id="plugin" name="plugin" required>
{{#each form.importers}}
<option value="{{this}}" {{#if (eq ../form.selected_importer this)}}selected{{/if}}>{{this}}</option>
{{/each}}
</select>
</div>
<div class="access-row-box">
<input type="file" id="files" name="files" required {{#if form.garage}} {{else}}multiple{{/if}} />
</div>
<div class="access-row-box">
<input type="submit" value="{{#if form.garage}}Override garage slot {{form.garage.slot}}{{else}}Import{{/if}}" />
</div>
</div>
</form>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Garage {{form.garage.slot}} - Openjam</title>
<meta name="title" content="Garages - Openjam" />
<meta name="description" content="User garage slot for third-party Robocraft servers" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Garage - Openjam" />
<meta property="og:description" content="View a garage for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Garages - Openjam" />
<meta property="twitter:description" content="View a garage for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
Welcome {{form.display_name}}! |
<a href="/">Home</a>
</div>
<h1 class="form-banner">{{form.display_name}} Garage #{{form.garage.slot}}</h1>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
<div class="access-list">
<div class="access-row-box">
<div class="access-grid">
<div class="access-box">
<a href="/garages/{{form.garage.garage_id}}/import">
<div class="access-box-banner">
Import
</div>
</a>
</div>
{{#each form.exporters}}
<div class="access-box">
<a href="/garages/{{../form.garage.garage_id}}/export?plugin={{this}}">
<div class="access-box-banner">
Export to <span class="export-plugin">{{this}}</span>
</div>
</a>
</div>
{{/each}}
<div class="access-box">
<a href="/garages/list">
<div class="access-box-banner">
Back to list
</div>
</a>
</div>
</div>
</div>
<div class="access-row-box">
<!--<h4 class="access-box-title">{{form.garage.name}}</h4>-->
<div class="box-details">
<div class="box-details-key">Name</div>
<div class="box-details-value">{{form.garage.name}}</div>
<div class="box-details-key">Slot</div>
<div class="box-details-value">{{form.garage.slot}}</div>
<div class="box-details-key">Database ID</div>
<div class="box-details-value">{{form.garage.garage_id}}</div>
</div>
</div>
</div>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Garages - Openjam</title>
<meta name="title" content="Garages - Openjam" />
<meta name="description" content="User garages for third-party Robocraft servers" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Garages - Openjam" />
<meta property="og:description" content="View your garages for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Garages - Openjam" />
<meta property="twitter:description" content="View your garages for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
Welcome {{form.display_name}}! |
<a href="/">Home</a>
</div>
<h1 class="form-banner">{{form.display_name}} Garages</h1>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
<div class="access-list">
<div class="access-row-box">
<div class="access-grid">
<div class="access-box">
<a href="/garages/import">
<div class="access-box-banner">
Import
</div>
</a>
</div>
<div class="access-box">
<a href="/garages/export">
<div class="access-box-banner">
Export
</div>
</a>
</div>
<div class="access-box">
<a href="/garages/selected">
<div class="access-box-banner">
Currently Selected
</div>
</a>
</div>
<div class="access-box">
<a href="http://127.0.0.1:8012">
<div class="access-box-banner">
Factory
</div>
</a>
</div>
</div>
</div>
{{#each form.garages}}
<div class="access-row-box">
<!--<h4 class="access-box-title">{{this.name}}</h4>-->
<div class="box-details">
<div class="box-details-key">Name</div>
<div class="box-details-value">{{this.name}}</div>
<div class="box-details-key">CPU</div>
<div class="box-details-value">{{this.cpu}}/{{this.max_cpu}}</div>
<div class="box-details-key">Slot</div>
<div class="box-details-value">{{this.slot}}</div>
<div class="box-details-key">Database ID</div>
<div class="box-details-value">{{this.garage_id}}</div>
<div class="box-details-key">Creation</div>
<div class="box-details-value"><time datetime="{{this.creation_time_iso}}">{{this.creation_time_iso}}</time></div>
</div>
<a href="/garages/{{this.garage_id}}/info">
<div class="access-box-banner">
View Details
</div>
</a>
</div>
{{/each}}
</div>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -22,132 +22,7 @@
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 90%;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 0 0 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-header-right {
position: relative;
width: 25%;
text-align: right;
margin: 0 0 0 auto;
box-sizing: border-box;
padding: 0 25px;
}
.form-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: dimgrey;
color: black;
padding: 25px;
}
.form-padded {
padding: 0 25px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
a { text-decoration-line: none; }
a:link { color: darkgrey; }
a:hover { color: white; }
a:visited { color: lightgrey; }
a:active { color: white; }
form {
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 250px;
}
.form-error {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-text-block {
width: 80%;
margin: 0 auto;
}
.access-grid {
width: 80%;
display: grid;
margin: 0 auto;
padding: 5px;
grid-template-columns: auto auto auto auto;
gap: 5px;
}
.access-box {
background-color: dimgrey;
color: black;
padding: 5px;
text-align: center;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
}
.access-box-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: black;
color: lightgrey;
padding: 5px;
}
.box-details {
text-align: left;
display: grid;
grid-template-columns: auto auto;
gap: 2px;
padding: 5px;
}
.box-details-key {
padding: 0 5px;
background-color: black;
color: lightgrey;
text-align: left;
}
.box-details-value {
padding: 0 5px;
background-color: lightgrey;
color: black;
text-align: left;
}
footer {
position: relative;
width: 100%
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">

View File

@@ -0,0 +1,158 @@
<!-- stylesheet.html.hbs start -->
<style>
body {
background-color: lightgrey;
padding: 0;
margin: 0;
}
.form-stripe {
position: relative;
min-width: 300px;
max-width: 90%;
height: auto;
min-height: 100%;
margin: 0 auto;
background-color: grey;
padding: 0 0 25px 0;
box-shadow: 0px 0px 50px 5px grey;
}
.form-header-right {
position: relative;
width: 25%;
text-align: right;
margin: 0 0 0 auto;
box-sizing: border-box;
padding: 0 25px;
}
.form-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: dimgrey;
color: black;
padding: 25px;
}
.form-padded {
padding: 0 25px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
a { text-decoration-line: none; }
a:link { color: darkgrey; }
a:hover { color: white; }
a:visited { color: lightgrey; }
a:active { color: white; }
form {
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 250px;
}
.form-error {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: orange;
}
.form-error-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-success {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: chartreuse;
}
.form-success-message {
padding: 25px;
margin: 0 auto;
width: 95%;
}
.form-text-block {
width: 80%;
margin: 0 auto;
}
.access-grid {
width: 80%;
display: grid;
margin: 0 auto;
padding: 5px;
grid-template-columns: auto auto auto auto;
gap: 5px;
}
.access-list {
width: 80%;
display: flex;
padding: 5px;
margin: 0 auto;
flex-direction: column;
gap: 5px;
}
.access-box {
background-color: dimgrey;
color: black;
padding: 5px 0;
text-align: center;
}
.access-row-box {
background-color: dimgrey;
color: black;
padding: 5px 0;
text-align: center;
width: 100%;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
}
.access-box-banner {
position: relative;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
background-color: black;
color: lightgrey;
padding: 5px;
}
.box-details {
text-align: left;
display: grid;
grid-template-columns: auto auto;
gap: 2px;
padding: 5px;
}
.box-details-key {
padding: 0 5px;
background-color: black;
color: lightgrey;
text-align: left;
}
.box-details-value {
padding: 0 5px;
background-color: lightgrey;
color: black;
text-align: left;
}
.export-plugin {
color: white;
}
footer {
position: relative;
width: 100%
margin: 0 auto;
background-color: black;
}
.footer {
width: 95%;
color: lightgrey;
box-sizing: border-box;
padding: 25px;
}
</style>
<!-- stylesheet.html.hbs end -->

View File

@@ -0,0 +1,334 @@
const COLOUR_COUNT: usize = 32;
struct ClassicModernTranslation {
id_mapping: ModernMapping,
offset: (i16, i16, i16),
orientation_mapping: OrientationMapping,
}
impl ClassicModernTranslation {
fn convert(&self, cube: &super::Cube) -> (super::Cube, super::Colour){
let (new_id, new_colour) = self.id_mapping.convert(cube);
let new_orientation = self.orientation_mapping.convert(cube);
let new_cube = super::Cube {
id: new_id,
x: (cube.x as u16).saturating_add_signed(self.offset.0) as _,
y: (cube.y as u16).saturating_add_signed(self.offset.1) as _,
z: (cube.z as u16).saturating_add_signed(self.offset.2) as _,
orientation: new_orientation,
};
let new_colour = super::Colour {
colour: new_colour,
x: new_cube.x,
y: new_cube.y,
z: new_cube.z,
};
(new_cube, new_colour)
}
}
#[allow(dead_code)]
enum ModernMapping {
Always(u32, u8),
ByOrientation(Vec<(u32, u8)>),
}
impl ModernMapping {
fn convert(&self, cube: &super::Cube) -> (u32, u8) {
match self {
Self::Always(id, colour) => (*id, *colour),
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
fn pretty(&self) -> String {
match self {
Self::Always(id, colour) => format!("ID:{}|colour#{}", id, colour),
Self::ByOrientation(mapping) => format!("o[0]ID:{}|colour#{}", mapping[0].0, mapping[0].1),
}
}
}
struct ModernClassicTranslation {
id_mapping: ClassicMapping,
offset: (i16, i16, i16),
orientation_mapping: OrientationMapping,
}
impl ModernClassicTranslation {
fn convert(&self, cube: &super::Cube, colour: &super::Colour) -> super::Cube {
let new_id = self.id_mapping.convert(cube, colour);
let new_orientation = self.orientation_mapping.convert(cube);
super::Cube {
id: new_id,
x: (cube.x as u16).saturating_add_signed(self.offset.0) as _,
y: (cube.y as u16).saturating_add_signed(self.offset.1) as _,
z: (cube.z as u16).saturating_add_signed(self.offset.2) as _,
orientation: new_orientation,
}
}
}
#[allow(dead_code)]
enum ClassicMapping {
Always(u32),
ByColour(Vec<u32>),
ByOrientation(Vec<u32>),
}
impl ClassicMapping {
fn convert(&self, cube: &super::Cube, colour: &super::Colour) -> u32 {
match self {
Self::Always(x) => *x,
Self::ByColour(v) => v[colour.colour as usize],
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
}
#[allow(dead_code)]
enum OrientationMapping {
Passthrough,
ByOrientation(Vec<u8>),
}
impl OrientationMapping {
fn convert(&self, cube: &super::Cube) -> u8 {
match self {
Self::Passthrough => cube.orientation,
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
}
#[derive(Debug)]
pub enum ConversionError {
UnknownCube {
id: u32,
index: usize,
position: (u8, u8, u8)
},
CubeParse(std::io::Error),
CubeDump(std::io::Error),
ColourParse(std::io::Error),
ColourDump(std::io::Error),
}
impl core::fmt::Display for ConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownCube { id, index, position } => write!(f, "Unknown cube ID {} at {} {:?}", id, index, position),
Self::CubeParse(e) => write!(f, "Failed to parse cube data: {}", e),
Self::CubeDump(e) => write!(f, "Failed to dump cube data: {}", e),
Self::ColourParse(e) => write!(f, "Failed to parse colour data: {}", e),
Self::ColourDump(e) => write!(f, "Failed to dump colour data: {}", e),
}
}
}
impl core::error::Error for ConversionError {}
pub struct ModernConversionResult{
pub cube_data: Vec<u8>,
pub colour_data: Vec<u8>,
}
pub struct CubeConversionParser {
classic_to_modern: std::collections::HashMap<u32, ClassicModernTranslation>,
modern_to_classic: std::collections::HashMap<u32, ModernClassicTranslation>,
known: std::collections::HashSet<u32>,
}
impl CubeConversionParser {
pub fn with_cubes<'a, I: std::iter::Iterator<Item=&'a crate::persist::Cube>>(cubes: I) -> Self {
let mut classic_to_modern = std::collections::HashMap::<u32, ClassicModernTranslation>::new();
let mut modern_to_classic = std::collections::HashMap::<u32, ModernClassicTranslation>::new();
let mut known = std::collections::HashSet::new();
for cube in cubes {
// populate classic_to_modern
let conversion_data = cube.conversion.clone().unwrap_or_else(crate::persist::conversion::actual_default_conversion);
for from in conversion_data.from.iter() {
let mapping = match from {
crate::persist::FromConversionData::Simple(_id) => ClassicModernTranslation {
id_mapping: ModernMapping::Always(cube.id, 0),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::FromConversionData::Complex { id: _, offset, colour } => ClassicModernTranslation {
id_mapping: ModernMapping::Always(cube.id, *colour),
offset: offset.unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
},
};
#[cfg(debug_assertions)]
if let Some(old_mapping) = classic_to_modern.get(&from.id()) {
log::debug!("Not overriding mapping classic ID {} from modern {} to modern {}", from.id(), old_mapping.id_mapping.pretty(), mapping.id_mapping.pretty());
} else {
classic_to_modern.insert(from.id(), mapping);
}
#[cfg(not(debug_assertions))]
if !classic_to_modern.contains_key(&from.id) {
classic_to_modern.insert(from.id(), mapping);
}
}
// populate modern_to_classic
let mapping = if let Some(to) = conversion_data.to {
match to {
crate::persist::ToConversionData::Simple(id) => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::ToConversionData::Complex { id, offset } => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(id),
offset: offset.unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
}
}
} else if !conversion_data.from.is_empty() {
if conversion_data.from.len() == 1 {
let from_first = conversion_data.from.first().unwrap();
match from_first {
crate::persist::FromConversionData::Simple(id) => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(*id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::FromConversionData::Complex { id, offset, colour: _ } => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(*id),
offset: offset.map(|(x, y, z)| (-x, -y, -z)).unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
},
}
} else {
// more than one conversion, build map
let default_id = conversion_data.from.first().unwrap().id();
let mut colour_map: Vec<u32> = (0..COLOUR_COUNT).map(|_| default_id).collect();
let mut is_default_set = false;
for from in conversion_data.from.iter() {
let colour = match from {
crate::persist::FromConversionData::Simple(_) => 0,
crate::persist::FromConversionData::Complex { colour, .. } => *colour,
};
if colour_map[colour as usize] != default_id { continue; }
if colour == 0 {
if !is_default_set {
is_default_set = true;
} else {
continue;
}
}
colour_map[colour as usize] = from.id();
}
ModernClassicTranslation {
id_mapping: ClassicMapping::ByColour(colour_map),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
}
}
} else {
ModernClassicTranslation {
id_mapping: ClassicMapping::Always(cube.id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
}
};
modern_to_classic.insert(cube.id, mapping);
// populate known cubes
known.insert(cube.id);
}
Self {
classic_to_modern,
modern_to_classic,
known,
}
}
/// Replace modern cubes with classic equivalents (based on cube ID)
pub fn convert_to_classic(&self, cubes: &mut dyn std::io::Read, colours: &mut dyn std::io::Read) -> Result<Vec<u8>, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let colour_data = super::parser::Colour::parse_list(colours).map_err(ConversionError::ColourParse)?;
for (i, (cube, colour)) in cube_data.iter_mut().zip(colour_data.iter()).enumerate() {
if let Some(translation) = self.modern_to_classic.get(&cube.id) {
let new_cube = translation.convert(cube, colour);
*cube = new_cube;
} else {
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
}
}
super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)
}
/// Replace classic cubes with modern equivalents (based on cube ID)
pub fn convert_to_modern(&self, cubes: &mut dyn std::io::Read) -> Result<ModernConversionResult, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let mut colour_data = Vec::with_capacity(cube_data.len());
for (i, cube) in cube_data.iter_mut().enumerate() {
if let Some(translation) = self.classic_to_modern.get(&cube.id) {
let (new_cube, new_colour) = translation.convert(cube);
log::trace!("cube {} RC15 ID {} -> Modern ID {} colour {} ({}, {}, {})", i, cube.id, new_cube.id, new_colour.colour, cube.x, cube.y, cube.z);
*cube = new_cube;
colour_data.push(new_colour);
} else if !self.known.contains(&cube.id){
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
} else {
log::debug!("No translation found for cube {} ({}, {}, {}) id {} (a valid modern ID)", i, cube.x, cube.y, cube.z, cube.id);
colour_data.push(super::Colour {
colour: 0,
x: cube.x,
y: cube.y,
z: cube.z,
});
}
}
#[cfg(debug_assertions)]
if cube_data.len() != colour_data.len() {
log::error!("Emitted different lengths of cube and colour data; {} cubes != {} colours", cube_data.len(), colour_data.len());
return Err(ConversionError::UnknownCube {
id: 0,
index: cube_data.len(),
position: (0, 0, 0),
});
}
let cube_bytes = super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)?;
let colour_bytes = super::parser::Colour::dump_list(colour_data).map_err(ConversionError::ColourDump)?;
Ok(ModernConversionResult {
cube_data: cube_bytes,
colour_data: colour_bytes,
})
}
/// Replace unknown cubes with modern equivalents (based on cube ID), skipping known cube IDs
pub fn upgrade_to_modern(&self, cubes: &mut dyn std::io::Read, colours: &mut dyn std::io::Read) -> Result<ModernConversionResult, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let mut colour_data = super::parser::Colour::parse_list(colours).map_err(ConversionError::ColourParse)?;
for (i, (cube, colour)) in cube_data.iter_mut().zip(colour_data.iter_mut()).enumerate() {
if self.known.contains(&cube.id) { continue; }
if let Some(translation) = self.classic_to_modern.get(&cube.id) {
let (new_cube, new_colour) = translation.convert(cube);
*cube = new_cube;
*colour = new_colour;
} else {
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
}
}
let cube_bytes = super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)?;
let colour_bytes = super::parser::Colour::dump_list(colour_data).map_err(ConversionError::ColourDump)?;
Ok(ModernConversionResult {
cube_data: cube_bytes,
colour_data: colour_bytes,
})
}
}

View File

@@ -1,4 +1,5 @@
mod parser;
pub use parser::{Colour, Cube};
mod weapon_list;
pub use weapon_list::WeaponListParser;
@@ -21,6 +22,9 @@ pub use rotations::CUBE_ROTATIONS;
mod graph;
pub use graph::{CubeGraph, CellPoint};
mod conversion;
pub use conversion::CubeConversionParser;
pub const CRYSTAL_ID: u32 = 3950293873;
pub const CLASP_ID: u32 = 606866102;
@@ -31,6 +35,7 @@ pub struct CubeParsers {
cpu_counter: std::sync::Arc<CpuListParser>,
locations: std::sync::Arc<CubeLocationsParser>,
offset: std::sync::Arc<OffsetParser>,
converter: std::sync::Arc<CubeConversionParser>,
}
impl CubeParsers {
@@ -41,6 +46,7 @@ impl CubeParsers {
cpu_counter: std::sync::Arc::new(CpuListParser::with_cubes(cubes.values())),
locations: std::sync::Arc::new(CubeLocationsParser::with_cubes(cubes.values())),
offset: std::sync::Arc::new(OffsetParser::with_cubes(cubes.values())),
converter: std::sync::Arc::new(CubeConversionParser::with_cubes(cubes.values())),
}
}
@@ -59,4 +65,8 @@ impl CubeParsers {
pub fn offset(&self) -> std::sync::Arc<OffsetParser> {
self.offset.clone()
}
pub fn converter(&self) -> std::sync::Arc<CubeConversionParser> {
self.converter.clone()
}
}

View File

@@ -28,17 +28,28 @@ impl CubeConfig {
pub fn load(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let file = std::fs::File::open(root.as_ref().join(CUBE_CONFIG_FILENAME))?;
let buffered = std::io::BufReader::new(file);
let result = serde_json::from_reader(buffered)?;
#[cfg(debug_assertions)]
{
let filename = root.as_ref().join(format!("{}.expanded.json", CUBE_CONFIG_FILENAME.trim_end_matches(".json")));
let file = std::fs::File::create(filename)?;
let buffered = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(buffered, &result)?;
let result = serde_json::from_reader::<_, Self>(buffered)?;
let result = result.expand_with_context();
let expanded_filename = root.as_ref().join(format!("{}.expanded.json", CUBE_CONFIG_FILENAME.trim_end_matches(".json")));
match std::fs::File::create(&expanded_filename) {
Ok(file) => {
let buffered = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(buffered, &result)?;
},
Err(e) => {
log::info!("Skipping creating {}: {}", expanded_filename.display(), e);
}
}
Ok(result)
}
/// Populate more of the config for parts of it which need more context for choosing default values
fn expand_with_context(mut self) -> Self {
// TODO
self.cubes.values_mut().for_each(crate::persist::conversion::expand_conversion_data);
self
}
/// Performs configuration checks
/// Returns true if validation succeeds, false if failed
pub fn self_validate(&self, data_path: impl AsRef<std::path::Path>) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ use serde::{Serialize, Deserialize};
use polariton::operation::Typed;
use super::{WeaponData, WeaponUpgradeInfo, TechTreeData, MovementData};
use super::{WeaponData, WeaponUpgradeInfo, TechTreeData, MovementData, CubeConversionData};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Cube {
@@ -14,6 +14,7 @@ pub struct Cube {
pub weapon_upgrade: Option<WeaponUpgradeInfo>,
pub movement: Option<MovementData>,
pub tree: Option<TechTreeData>,
pub conversion: Option<CubeConversionData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@@ -50,6 +50,9 @@ pub use team_chooser::TeamChooser;
mod vehicle_validator;
pub use vehicle_validator::VehicleValidator;
pub(crate) mod conversion;
pub use conversion::{CubeConversionData, FromConversionData, ToConversionData};
const VALID_ROBOT: &[u8] = &[64,
0,
0,

View File

@@ -396,7 +396,7 @@ impl UserData {
Ok(())
}
async fn all_vehicles(&self) -> Result<Vec<oj_rc_database::schema::garage::Model>, oj_rc_database::sea_orm::DbErr> {
pub(super) async fn all_vehicles(&self) -> Result<Vec<oj_rc_database::schema::garage::Model>, oj_rc_database::sea_orm::DbErr> {
self.db.garages_by_user_id(self.account.id).await
}
@@ -1123,7 +1123,7 @@ impl <C: Clone + Send> super::User<C> for UserData {
let total_cost: u32 = self.garage_upgrades.increments.iter()
.map(|x| if x.cpu > minimum_upgrade_to_cpu { 0 } else { x.cost })
.sum();
log::info!("Bay CPU upgrade costs {}", total_cost);
log::debug!("Bay CPU upgrade costs {}", total_cost);
self.currency_sub_checked(super::CurrencyType::Free, total_cost as u64).await.map_err(|e| {
log::error!("Failed to debit user for cpu upgrade during fresh clone to slot {} for user_id {}: {}", slot, self.account.id, e);
DATABASE_ERR

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;

View File

@@ -676,7 +676,19 @@ pub trait FactoryUser {
#[async_trait::async_trait]
pub trait WebUser: CommonUser {
async fn garages(&self) -> Result<Vec<GarageWebInfo>, Box<dyn std::error::Error>>;
async fn garage_by_id(&self, id: i32) -> Result<Option<VehicleData>, Box<dyn std::error::Error>>;
async fn save_garage(&self, vehicle: crate::persist::user::VehicleData, garage_id: Option<i32>, cpu_counter: &crate::cubes::CpuListParser, weapon_orderer: &crate::cubes::WeaponListParser) -> Result<(), Box<dyn std::error::Error>>;
async fn garage_id_selected(&self) -> Result<Option<i32>, Box<dyn std::error::Error>>;
}
pub struct GarageWebInfo {
pub id: i32,
pub slot: i32,
pub total_robot_cpu: i32,
pub bay_cpu: i32,
pub name: String,
pub creation_time: i64,
}
#[async_trait::async_trait]

View File

@@ -1,4 +1,109 @@
#[async_trait::async_trait]
impl super::WebUser for super::account_json::UserData {
// TODO
async fn garages(&self) -> Result<Vec<super::GarageWebInfo>, Box<dyn std::error::Error>> {
let vehicles = self.all_vehicles().await?;
Ok(vehicles.into_iter()
.map(|v| super::GarageWebInfo {
id: v.id,
slot: v.slot,
total_robot_cpu: v.total_robot_cpu,
bay_cpu: v.bay_cpu,
name: v.name.clone(),
creation_time: v.creation_time,
})
.collect()
)
}
async fn garage_by_id(&self, id: i32) -> Result<Option<super::VehicleData>, Box<dyn std::error::Error>> {
let garage = self.db.garage_by_id(id).await?;
Ok(garage.and_then(|g|
if g.user_id == self.account.id {
Some(super::VehicleData {
name: Some(g.name),
slot: g.slot,
robot_data: g.robot_data,
colour_data: g.colour_data,
weapon_order: Vec::default(),
crf_id: g.crf_id,
was_rated: Some(g.was_rated),
})
} else {
None
}
))
}
async fn save_garage(
&self,
vehicle: super::VehicleData,
garage_id: Option<i32>,
cpu_counter: &crate::cubes::CpuListParser,
weapon_orderer: &crate::cubes::WeaponListParser,
) -> Result<(), Box<dyn std::error::Error>> {
let cpu_counts = cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&vehicle.robot_data));
let weapon_order = weapon_orderer.guess_weapons(&mut std::io::Cursor::new(&vehicle.robot_data));
let mut entity = oj_rc_database::schema::garage::ActiveModel {
id: garage_id.map(oj_rc_database::sea_orm::ActiveValue::Set).unwrap_or(oj_rc_database::sea_orm::ActiveValue::NotSet),
user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id),
weapon_order: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::dump_csv(&weapon_order)),
robot_data: oj_rc_database::sea_orm::ActiveValue::Set(vehicle.robot_data),
colour_data: oj_rc_database::sea_orm::ActiveValue::Set(vehicle.colour_data),
crf_id: oj_rc_database::sea_orm::ActiveValue::NotSet,
name: if let Some(new_name) = vehicle.name {
oj_rc_database::sea_orm::ActiveValue::Set(format!("{} (imported)", new_name))
} else {
oj_rc_database::sea_orm::ActiveValue::Set(format!(
"Import {}",
garage_id.map(|x| x.to_string()).unwrap_or_else(|| "(new)".to_owned())
))
},
total_robot_cpu: oj_rc_database::sea_orm::ActiveValue::Set(cpu_counts.total as _),
total_cosmetic_cpu: oj_rc_database::sea_orm::ActiveValue::Set(cpu_counts.cosmetic as _),
was_rated: oj_rc_database::sea_orm::ActiveValue::Set(false),
movement_categories: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()), // FIXME
total_robot_ranking: oj_rc_database::sea_orm::ActiveValue::Set(0), // FIXME
bay_cpu: oj_rc_database::sea_orm::ActiveValue::Set(10_000),
//tutorial_robot: oj_rc_database::sea_orm::ActiveValue::Set(false),
mastery_level: oj_rc_database::sea_orm::ActiveValue::Set(0),
..super::initial_data::default_reset_slot()
};
// TODO charge currency for new or higher CPU bay
/*let total_cost: u32 = self.garage_upgrades.increments.iter()
.map(|x| if x.cpu > minimum_upgrade_to_cpu { 0 } else { x.cost })
.sum();
log::debug!("Bay CPU upgrade costs {}", total_cost);
self.currency_sub_checked(super::CurrencyType::Free, total_cost as u64).await.map_err(|e| {
log::error!("Failed to debit user for cpu upgrade during create of slot {} for user_id {}: {}", slot, self.account.id, e);
DATABASE_ERR
})?;*/
if let Some(garage_id) = garage_id {
// technically we don't need an account id to get the slot
// but this way ensures the user also owns the slot we retrieve
// (and no slot will be retrieved if the user doesn't own the slot, even if the garage id exists)
if let Some(slot) = self.db.slot_of_garage_by_id_and_user_id(garage_id, self.account.id).await? {
entity.slot = oj_rc_database::sea_orm::ActiveValue::Set(slot);
self.db.update_garage_by_user_id_and_slot(entity, self.account.id, slot).await?;
Ok(())
} else {
Err(format!("Invalid vehicle ID {} for user {}", garage_id, self.account.id).into())
}
} else {
let now = chrono::Utc::now().timestamp();
let new_slot = self.db.garage_max_slot_by_user_id(self.account.id).await? + 1;
let uuid = super::uuid_sanitize(now);
entity.slot = oj_rc_database::sea_orm::ActiveValue::Set(new_slot);
entity.creation_time = oj_rc_database::sea_orm::ActiveValue::Set(now);
entity.uuid = oj_rc_database::sea_orm::ActiveValue::Set(uuid);
entity.thumbnail_version = oj_rc_database::sea_orm::ActiveValue::Set(0);
entity.selected = oj_rc_database::sea_orm::ActiveValue::Set(false);
self.db.insert_garage(entity).await?;
Ok(())
}
}
async fn garage_id_selected(&self) -> Result<Option<i32>, Box<dyn std::error::Error>> {
Ok(self.db.garage_selected(self.account.id).await?
.map(|x| x.id))
}
}

View File

@@ -129,7 +129,7 @@ impl Database {
}
pub async fn insert_user_aux(&self, entities: Vec<crate::schema::user_aux::ActiveModel>) -> Result<(), sea_orm::DbErr> {
crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?;
crate::schema::user_aux::Entity::insert_many(entities).exec(self.orm.as_ref()).await?;
Ok(())
}
@@ -217,6 +217,13 @@ impl Database {
.await
}
pub async fn garage_count_by_user_id(&self, user_id: i32) -> Result<u64, sea_orm::DbErr> {
crate::schema::garage::Entity::find()
.filter(crate::schema::garage::Column::UserId.eq(user_id))
.count(self.orm.as_ref())
.await
}
pub async fn garage_max_slot_by_user_id(&self, user_id: i32) -> Result<i32, sea_orm::DbErr> {
let result = crate::schema::garage::Entity::find()
.select_only()
@@ -265,8 +272,20 @@ impl Database {
.await
}
pub async fn slot_of_garage_by_id_and_user_id(&self, garage_id: i32, user_id: i32) -> Result<Option<i32>, sea_orm::DbErr> {
let opt = crate::schema::garage::Entity::find()
.select_only()
.column_as(crate::schema::garage::Column::Slot, "column")
.filter(crate::schema::garage::Column::Id.eq(garage_id))
.filter(crate::schema::garage::Column::UserId.eq(user_id))
.into_model::<crate::schema::common_query::SingleColumn<i32>>()
.one(self.orm.as_ref())
.await?;
Ok(opt.map(|x| *x))
}
pub async fn insert_garages(&self, entities: Vec<crate::schema::garage::ActiveModel>) -> Result<(), sea_orm::DbErr> {
crate::schema::garage::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?;
crate::schema::garage::Entity::insert_many(entities).exec(self.orm.as_ref()).await?;
Ok(())
}
@@ -515,7 +534,7 @@ impl Database {
}
pub async fn insert_players(&self, entities: Vec<crate::schema::multiplayer_game_player::ActiveModel>) -> Result<(), sea_orm::DbErr> {
crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?;
crate::schema::multiplayer_game_player::Entity::insert_many(entities).exec(self.orm.as_ref()).await?;
Ok(())
}

View File

@@ -658,7 +658,7 @@ impl QueueHandler {
count += q_entry.users.len();
if let Some(values) = new_queue_map.get_mut(&key) {
values.users.append(&mut q_entry.users);
values.platoons.extend(q_entry.platoons.into_iter());
values.platoons.extend(q_entry.platoons);
} else {
new_queue_map.insert(key.clone(), q_entry);
}

View File

@@ -1,6 +1,7 @@
pub mod chat;
pub mod vehicle_validation;
pub mod team_selection;
pub mod vehicle_import;
pub trait Plugin: Send + Sync {
fn self_check(&self) -> bool {

View File

@@ -0,0 +1,2 @@
mod plugin;
pub use plugin::{VehicleImportPlugin, VehicleImportErrorCode, VehicleImportData};

View File

@@ -0,0 +1,35 @@
#[repr(u8)]
#[derive(Debug)]
pub enum VehicleImportErrorCode {
Invalid,
Unsupported,
UnknownCube,
}
impl core::fmt::Display for VehicleImportErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Invalid => write!(f, "Invalid/corrupt data"),
Self::Unsupported => write!(f, "Unsupported/unrecognized data"),
Self::UnknownCube => write!(f, "Unknown cube in data"),
}
}
}
impl core::error::Error for VehicleImportErrorCode {}
#[derive(Debug)]
pub struct VehicleImportData {
pub cube_data: Vec<u8>,
pub colour_data: Vec<u8>,
pub vehicle_name: Option<String>,
pub vehicle_author: Option<String>,
}
pub trait VehicleImportPlugin: crate::Plugin {
//fn supports(&self) -> Vec<String>;
/// Preferred file extension for the raw data
fn file_ext(&self) -> &'static str;
fn import(&self, upload: &[u8]) -> Result<VehicleImportData, VehicleImportErrorCode>;
fn export(&self, data: &VehicleImportData) -> Result<Vec<u8>, VehicleImportErrorCode>;
}

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()
))