Compare commits

..

No commits in common. "2738de85db5d788936ce6f38f065bdf63b513eff" and "cb92ffe3dfd28e154997b590fe53f3132375e519" have entirely different histories.

51 changed files with 495 additions and 12283 deletions

4
.gitignore vendored
View File

@ -2,7 +2,3 @@
todo.org todo.org
*.sqlite* *.sqlite*
*.sqlite.* *.sqlite.*
node_modules
static/scripts/dist
static/templates/*
generated/

344
Cargo.lock generated
View File

@ -98,27 +98,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-stream"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5"
dependencies = [
"async-stream-impl",
"futures-core",
]
[[package]]
name = "async-stream-impl"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.42" version = "0.1.42"
@ -177,12 +156,6 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@ -373,7 +346,7 @@ version = "0.15.0-dev"
source = "git+https://github.com/SergioBenitez/cookie-rs.git?rev=1c3ca83#1c3ca838543b60a4448d279dc4b903cc7a2bc22a" source = "git+https://github.com/SergioBenitez/cookie-rs.git?rev=1c3ca83#1c3ca838543b60a4448d279dc4b903cc7a2bc22a"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"base64 0.13.0", "base64",
"hkdf", "hkdf",
"percent-encoding", "percent-encoding",
"rand 0.7.3", "rand 0.7.3",
@ -1695,7 +1668,6 @@ dependencies = [
"rand_chacha 0.2.2", "rand_chacha 0.2.2",
"rand_core 0.5.1", "rand_core 0.5.1",
"rand_hc 0.2.0", "rand_hc 0.2.0",
"rand_pcg",
] ]
[[package]] [[package]]
@ -1766,15 +1738,6 @@ dependencies = [
"rand_core 0.6.0", "rand_core 0.6.0",
] ]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.57" version = "0.1.57"
@ -1872,21 +1835,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "ring"
version = "0.16.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "rocket" name = "rocket"
version = "0.5.0-dev" version = "0.5.0-dev"
@ -1962,7 +1910,6 @@ dependencies = [
"state", "state",
"time 0.2.23", "time 0.2.23",
"tokio", "tokio",
"tokio-rustls",
"uncased", "uncased",
"unicode-xid", "unicode-xid",
"version_check", "version_check",
@ -1989,7 +1936,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
dependencies = [ dependencies = [
"base64 0.13.0", "base64",
"blake2b_simd", "blake2b_simd",
"constant_time_eq", "constant_time_eq",
"crossbeam-utils", "crossbeam-utils",
@ -2004,19 +1951,6 @@ dependencies = [
"semver 0.9.0", "semver 0.9.0",
] ]
[[package]]
name = "rustls"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
dependencies = [
"base64 0.12.3",
"log",
"ring",
"sct",
"webpki",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -2048,16 +1982,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.0.0" version = "2.0.0"
@ -2226,12 +2150,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "sqlformat" name = "sqlformat"
version = "0.1.5" version = "0.1.5"
@ -2485,13 +2403,11 @@ dependencies = [
"rocket_contrib", "rocket_contrib",
"rust-argon2", "rust-argon2",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"sqlx", "sqlx",
"strum", "strum",
"thiserror", "thiserror",
"tokio",
"tonic",
"tonic-build",
] ]
[[package]] [[package]]
@ -2653,18 +2569,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a"
dependencies = [
"futures-core",
"rustls",
"tokio",
"webpki",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.3.1" version = "0.3.1"
@ -2688,226 +2592,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "tonic"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74a5d6e7439ecf910463667080de772a9c7ddf26bc9fb4f3252ac3862e43337d"
dependencies = [
"async-stream",
"async-trait",
"base64 0.12.3",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"percent-encoding",
"pin-project 0.4.27",
"prost",
"prost-derive",
"tokio",
"tokio-util",
"tower",
"tower-balance",
"tower-load",
"tower-make",
"tower-service",
"tracing",
"tracing-futures",
]
[[package]]
name = "tonic-build"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19970cf58f3acc820962be74c4021b8bbc8e8a1c4e3a02095d0aa60cde5f3633"
dependencies = [
"proc-macro2",
"prost-build",
"quote",
"syn",
]
[[package]]
name = "tower"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3169017c090b7a28fce80abaad0ab4f5566423677c9331bb320af7e49cfe62"
dependencies = [
"futures-core",
"tower-buffer",
"tower-discover",
"tower-layer",
"tower-limit",
"tower-load-shed",
"tower-retry",
"tower-service",
"tower-timeout",
"tower-util",
]
[[package]]
name = "tower-balance"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a792277613b7052448851efcf98a2c433e6f1d01460832dc60bef676bc275d4c"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"pin-project 0.4.27",
"rand 0.7.3",
"slab",
"tokio",
"tower-discover",
"tower-layer",
"tower-load",
"tower-make",
"tower-ready-cache",
"tower-service",
"tracing",
]
[[package]]
name = "tower-buffer"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4887dc2a65d464c8b9b66e0e4d51c2fd6cf5b3373afc72805b0a60bce00446a"
dependencies = [
"futures-core",
"pin-project 0.4.27",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-discover"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6b5000c3c54d269cc695dff28136bb33d08cbf1df2c48129e143ab65bf3c2a"
dependencies = [
"futures-core",
"pin-project 0.4.27",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35d656f2638b288b33495d1053ea74c40dc05ec0b92084dd71ca5566c4ed1dc"
[[package]]
name = "tower-limit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92c3040c5dbed68abffaa0d4517ac1a454cd741044f33ab0eefab6b8d1361404"
dependencies = [
"futures-core",
"pin-project 0.4.27",
"tokio",
"tower-layer",
"tower-load",
"tower-service",
]
[[package]]
name = "tower-load"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cc79fc3afd07492b7966d7efa7c6c50f8ed58d768a6075dd7ae6591c5d2017b"
dependencies = [
"futures-core",
"log",
"pin-project 0.4.27",
"tokio",
"tower-discover",
"tower-service",
]
[[package]]
name = "tower-load-shed"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f021e23900173dc315feb4b6922510dae3e79c689b74c089112066c11f0ae4e"
dependencies = [
"futures-core",
"pin-project 0.4.27",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-make"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce50370d644a0364bf4877ffd4f76404156a248d104e2cc234cd391ea5cdc965"
dependencies = [
"tokio",
"tower-service",
]
[[package]]
name = "tower-ready-cache"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eabb6620e5481267e2ec832c780b31cad0c15dcb14ed825df5076b26b591e1f"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"log",
"tokio",
"tower-service",
]
[[package]]
name = "tower-retry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6727956aaa2f8957d4d9232b308fe8e4e65d99db30f42b225646e86c9b6a952"
dependencies = [
"futures-core",
"pin-project 0.4.27",
"tokio",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860"
[[package]]
name = "tower-timeout"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "127b8924b357be938823eaaec0608c482d40add25609481027b96198b2e4b31e"
dependencies = [
"pin-project 0.4.27",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-util"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1093c19826d33807c72511e68f73b4a0469a3f22c2bd5f7d5212178b4b89674"
dependencies = [
"futures-core",
"futures-util",
"pin-project 0.4.27",
"tower-service",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.22" version = "0.1.22"
@ -2917,21 +2607,9 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"log", "log",
"pin-project-lite 0.2.0", "pin-project-lite 0.2.0",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.17" version = "0.1.17"
@ -3083,12 +2761,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.0" version = "2.2.0"
@ -3210,16 +2882,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "which" name = "which"
version = "3.1.1" version = "3.1.1"

View File

@ -6,9 +6,6 @@ edition = "2018"
build = "build.rs" build = "build.rs"
default-run = "tenebrous" default-run = "tenebrous"
[package.metadata.scripts]
grpc-proxy = 'docker run -d --rm -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro --network=host envoyproxy/envoy:v1.16.1'
[[bin]] [[bin]]
name = "tenebrous-migrate" name = "tenebrous-migrate"
path = "src/migrate.rs" path = "src/migrate.rs"
@ -19,12 +16,11 @@ path = "src/main.rs"
[build-dependencies] [build-dependencies]
prost-build = "0.6" prost-build = "0.6"
tonic-build = "0.3"
[dependencies] [dependencies]
tonic = "0.3"
prost = "0.6" prost = "0.6"
serde = { version = "1.0", features = ["derive"] } serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
erased-serde = "0.3" erased-serde = "0.3"
thiserror = "1.0" thiserror = "1.0"
@ -32,7 +28,6 @@ rust-argon2 = "0.8"
log = "0.4" log = "0.4"
rand = "0.7" rand = "0.7"
futures = "0.3" futures = "0.3"
tokio = { version = "0.2", features = ["macros"] }
strum = { version = "0.20", features = ["derive"] } strum = { version = "0.20", features = ["derive"] }
sqlx = { version = "0.4.2", features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] } sqlx = { version = "0.4.2", features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] }
refinery = { version = "0.3", features = ["rusqlite"]} refinery = { version = "0.3", features = ["rusqlite"]}
@ -41,7 +36,7 @@ barrel = { version = "0.6", features = ["sqlite3"] }
[dependencies.rocket] [dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"
branch = "master" branch = "master"
features = ["secrets", "tls"] features = ["secrets"]
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"

View File

@ -4,66 +4,41 @@ An open source character sheet service for tabletop roleplaying games.
Currently under heavy development. Currently under heavy development.
## The Stack
This project makes use of these technologies:
- Rust
- Rocket Web Framework
- Tonic gRPC Framework
- Typescript
Building is backed by: cargo, npm, and webpack.
## Build Instructions ## Build Instructions
These are very basic build instructions. They assume you already have These are very basic build instructions. They assume you already have
cargo set up and installed. cargo set up and installed.
### Initial Setup ### Install Dependencies
Quick initial setup instructions that will set up a development Install dependencies. The exact method depends on your OS.
environment.
First, install dependencies by either using the command below (Void
Linux), or reading the "Dependencies Required" section:
```
xbps-install sqlite sqlite-devel protobuf nodejs docker
```
Then run the required `cargo` commands to install management tools and
create a development database:
```
cargo install --version=0.2.0 sqlx-cli
cargo install cargo-run-script
cargo run --bin tenebrous-migrate
```
### Dependencies Required
Dependencies required to build the project. The exact installation
method depends on your OS.
* sqlite3 and development headers (Void Linux: `xbps-install * sqlite3 and development headers (Void Linux: `xbps-install
sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`). sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`).
* protoc: protocol buffers compiler. Needed to compile protobuf files * protoc: protocol buffers compiler. There is one baked into the
for both the server and web frontends (Void Linux: `xbps-install build, so you should not need this unless you are not using
protobuf`). Linux/Mac/Windows.
* Node and npm: Needed to run webpack for compiling and injecting
Typescript into various web pages (Void Linux: `xbps-install ### Initial Setup
nodejs`).
* Docker: Needed to run the grpc proxy (Void Linux `xbps-install Follow these instructions from the root of the repository.
docker`).
Set up database:
```
cargo install --version=0.2.0 sqlx-cli
cargo run --bin tenebrous-migrate
```
### Run Application ### Run Application
If you are using `rustup`, then it should automatically switch to the
stable version of Rust in this repository. This is because of the
`rust-toolchain` file.
Command line "instructions" to build and run the application: Command line "instructions" to build and run the application:
``` ```
cargo run-script grpc-proxy # only required if proxy not already running
cargo run cargo run
``` ```
@ -90,15 +65,4 @@ to update the SQLx data JSON file:
cargo sqlx prepare -- --bin tenebrous cargo sqlx prepare -- --bin tenebrous
``` ```
### gRPC-Web Proxy
The frontend web application makes use of the gRPC-Web protocol to
call gPRC endpoints from the browser. This requires a proxy to
translate the calls from the browser to HTTP2 calls gRPC understands.
For development, executing the `cargo run-script grpc-proxy` command
will start the envoy proxy recommended by Google's gRPC-Web project.
The envoy configuration assumes you are on Linux. If you are using Mac
OS or Windows, see the note in the envoy configuration.
[rustup]: https://rust-lang.github.io/rustup/index.html [rustup]: https://rust-lang.github.io/rustup/index.html

View File

@ -1,6 +1,3 @@
[default]
template_dir = "generated/templates/"
[development] [development]
address = "localhost" address = "localhost"
port = 8000 port = 8000

View File

@ -1,50 +1,10 @@
use std::process::Command;
fn js_protos() {
let output = Command::new("npm")
.arg("run")
.arg("build:protobuf")
.output()
.unwrap();
if output.status.success() {
return;
} else {
let err = String::from_utf8(output.stderr).unwrap();
panic!("JS Protobuf build failure:\n {}", err);
}
}
fn webpack() {
let output = Command::new("npm")
.arg("run")
.arg("build:webpack")
.output()
.unwrap();
if output.status.success() {
return;
} else {
let err = String::from_utf8(output.stderr).unwrap();
panic!("Webpack build failure:\n {}", err);
}
}
fn main() { fn main() {
println!("cargo:rerun-if-changed=static/scripts/webpack.config.js");
js_protos();
webpack();
let mut config = prost_build::Config::new(); let mut config = prost_build::Config::new();
config.btree_map(&["."]); config.btree_map(&["."]);
config.type_attribute(".", "#[derive(::serde::Serialize)]"); config.type_attribute(".", "#[derive(Serialize)]");
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
config
tonic_build::configure() .compile_protos(
.build_server(false)
.build_client(false)
.compile_with_config(
config,
&["proto/cofd.proto", "proto/cofd_api.proto"], &["proto/cofd.proto", "proto/cofd_api.proto"],
&["src/", "proto/"], &["src/", "proto/"],
) )

View File

@ -1,54 +0,0 @@
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: greeter_service
max_grpc_timeout: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
clusters:
- name: greeter_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: localhost
port_value: 9090

View File

@ -1,3 +0,0 @@
FROM envoyproxy/envoy:v1.16-latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

5361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +0,0 @@
{
"name": "tenebrous-sheets",
"version": "0.1.0",
"description": "An open source character sheet service for tabletop roleplaying games.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"webpack-dev": "webpack --watch",
"build:protobuf": "mkdir -p src/frontend/_proto && protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts -I ./proto --js_out=import_style=commonjs,binary:./src/frontend/_proto --ts_out=service=grpc-web:./src/frontend/_proto ./proto/*.proto",
"build:webpack": "webpack"
},
"repository": {
"type": "git",
"url": "ssh://git@git.agnos.is:22022/projectmoon/tenebrous-sheets.git"
},
"author": "",
"license": "AGPL",
"dependencies": {
"@improbable-eng/grpc-web": "^0.13.0",
"google-protobuf": "^3.14.0"
},
"devDependencies": {
"@types/google-protobuf": "^3.7.4",
"clean-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^4.5.0",
"ts-loader": "^8.0.13",
"ts-protoc-gen": "^0.14.0",
"typescript": "^4.1.3",
"webpack": "^5.11.1",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.1"
}
}

View File

@ -57,47 +57,43 @@ message CofdSheet {
} }
string name = 1; string name = 1;
string gender = 2; string player = 2;
string concept = 3; string campaign = 3;
int32 age = 4; string description = 4;
string player = 5; int32 strength = 6;
string chronicle = 6; int32 dexterity = 7;
string description = 7; int32 stamina = 8;
int32 strength = 8; int32 intelligence = 9;
int32 dexterity = 9; int32 wits = 10;
int32 stamina = 10; int32 resolve = 11;
int32 intelligence = 11; int32 presence = 12;
int32 wits = 12; int32 manipulation = 13;
int32 resolve = 13; int32 composure = 14;
int32 presence = 14; map<string, Skill> physical_skills = 16;
int32 manipulation = 15; map<string, Skill> mental_skills = 17;
int32 composure = 16; map<string, Skill> social_skills = 18;
map<string, Skill> physical_skills = 17; repeated Merit merits = 15;
map<string, Skill> mental_skills = 18; repeated Condition conditions = 19;
map<string, Skill> social_skills = 19;
repeated Merit merits = 20; int32 size = 20;
repeated Condition conditions = 21; int32 health = 21;
int32 willpower = 22;
int32 experience_points = 23;
int32 beats = 24;
int32 size = 22; repeated Item items = 25;
int32 health = 23; repeated Attack attacks = 26;
int32 willpower = 24;
int32 experience_points = 25;
int32 beats = 26;
repeated Item items = 27; map<string, string> other_data = 27;
repeated Attack attacks = 28;
map<string, string> other_data = 29;
oneof system_fields { oneof system_fields {
CoreFields core = 30; CoreFields core = 28;
MageFields mage = 31; MageFields mage = 29;
ChangelingFields changeling = 32; ChangelingFields changeling = 30;
} }
} }

View File

@ -3,70 +3,77 @@ import "cofd.proto";
package models.proto.cofd.api; package models.proto.cofd.api;
message CharacterIdentifier {
string owner = 1;
int32 id = 2;
}
//Update basic information about a Chronicles of Darkness (or //Update basic information about a Chronicles of Darkness (or
//derivative system) character sheet. This is a straight overwrite of //derivative system) character sheet. This is a straight overwrite of
//all basic information on the sheet. //all basic information on the sheet.
message UpdateBasicInfoRequest { message BasicInfo {
CharacterIdentifier character = 1; string name = 1;
reserved 2; string gender = 2;
// string owner = 1; string concept = 3;
// int32 character_id = 2; string chronicle = 4;
int32 age = 5;
string name = 3;
string gender = 4;
string concept = 5;
string chronicle = 6;
int32 age = 7;
} }
//Generic "did something succeed or not" response. //Update all attributes in a Chronicles of Darkness character (or
message ApiResult { //derivative system) character sheet. This is a straight overwrite of
bool success = 1; //all basic information on the sheet.
string error = 2; message Attributes {
int32 strength = 1;
int32 dexterity = 2;
int32 stamina = 3;
int32 intelligence = 4;
int32 wits = 5;
int32 resolve = 6;
int32 presence = 7;
int32 manipulation = 8;
int32 composure = 9;
} }
//Update an attribute's dot amount. TODO rename to AttributesUpdate. //Update an attribute's dot amount. TODO rename to AttributesUpdate.
message UpdateAttributeRequest { message UpdateAttributeRequest {
CharacterIdentifier character = 1; string character_username = 1;
string attribute_name = 2; int32 character_id = 2;
int32 attribute_value = 3; string attribute_name = 3;
int32 attribute_value = 4;
}
//Update skill entries in a Chronicles of Darkness character sheet.
//This is a straight overwrite of all skills in the sheet.
message Skills {
repeated CofdSheet.Skill physical_skills = 1;
repeated CofdSheet.Skill mental_skills = 2;
repeated CofdSheet.Skill social_skills = 3;
} }
//Full update of a single skill //Full update of a single skill
message UpdateSkillRequest { message SkillUpdate {
CharacterIdentifier character = 1; string name = 1;
CofdSheet.Skill skill = 2; CofdSheet.Skill skill = 2;
} }
//Partial update of a single skill dot amount. //Partial update of a single skill dot amount.
message UpdateSkillValueRequest { message UpdateSkillValueRequest {
CharacterIdentifier character = 1; string character_username = 1;
string skill_name = 2; int32 character_id = 2;
int32 skill_value = 3; string skill_name = 3;
int32 skill_value = 4;
} }
//Partial update of only a skill's specializations. The //Partial update of only a skill's specializations. The
//specializations will be overwritten with the new values. //specializations will be overwritten with the new values.
message UpdateSkillSpecializationsRequest { message SkillSpecializationsUpdate {
string name = 1; string name = 1;
repeated string specializations = 2; repeated string specializations = 2;
} }
//Add a Condition to a Chronicles of Darkness character sheet. //Add a Condition to a Chronicles of Darkness character sheet.
message AddConditionRequest { message Condition {
string character_username = 1; string name = 1;
int32 character_id = 2;
string condition_name = 3;
} }
//Remove a Condition from a Chronicles of Darkness character sheet. service CofdApi {
message RemoveConditionRequest { rpc UpdateSkillValue(UpdateSkillValueRequest) returns (CofdSheet.Skill);
string character_username = 1;
int32 character_id = 2;
string condition_name = 3;
} }

View File

@ -39,8 +39,6 @@ pub(crate) trait Dao {
async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()>; async fn insert_character(&self, new_character: NewCharacter<'_>) -> sqlx::Result<()>;
async fn update_character<'a>(&self, character: &'a Character) -> sqlx::Result<()>;
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()>; async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()>;
} }
@ -126,25 +124,6 @@ impl Dao for SqlitePool {
Ok(()) Ok(())
} }
async fn update_character<'a>(&self, character: &'a Character) -> sqlx::Result<()> {
sqlx::query(
"UPDATE characters
set user_id = ?, viewable = ?, character_name = ?,
data_type = ?, data_version = ?, data = ? where id = ?",
)
.bind(character.user_id)
.bind(character.viewable)
.bind(&character.character_name)
.bind(character.data_type)
.bind(character.data_version)
.bind(&character.data)
.bind(character.id)
.execute(self)
.await?;
Ok(())
}
async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()> { async fn update_character_sheet<'a>(&self, character: &'a Character) -> sqlx::Result<()> {
sqlx::query("UPDATE characters set data = ? where id = ?") sqlx::query("UPDATE characters set data = ? where id = ?")
.bind(&character.data) .bind(&character.data)

View File

@ -4,7 +4,6 @@ use rocket::response::status;
use rocket::response::{self, Responder}; use rocket::response::{self, Responder};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Into;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -49,28 +48,6 @@ impl Error {
_ => false, _ => false,
} }
} }
fn message(&self) -> String {
if self.is_sensitive() {
"internal error".to_string()
} else {
self.to_string()
}
}
}
impl From<Error> for tonic::Status {
fn from(err: Error) -> tonic::Status {
use tonic::{Code, Status};
use Error::*;
match err {
NotFound => Status::new(Code::NotFound, err.message()),
NotLoggedIn => Status::new(Code::Unauthenticated, err.message()),
NoPermission => Status::new(Code::PermissionDenied, err.message()),
InvalidInput => Status::new(Code::InvalidArgument, err.message()),
_ => Status::new(Code::Internal, err.message()),
}
}
} }
#[rocket::async_trait] #[rocket::async_trait]

View File

@ -1,266 +0,0 @@
// package: models.proto.cofd.api
// file: cofd_api.proto
import * as jspb from "google-protobuf";
import * as cofd_pb from "./cofd_pb";
export class CharacterIdentifier extends jspb.Message {
getOwner(): string;
setOwner(value: string): void;
getId(): number;
setId(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CharacterIdentifier.AsObject;
static toObject(includeInstance: boolean, msg: CharacterIdentifier): CharacterIdentifier.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: CharacterIdentifier, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): CharacterIdentifier;
static deserializeBinaryFromReader(message: CharacterIdentifier, reader: jspb.BinaryReader): CharacterIdentifier;
}
export namespace CharacterIdentifier {
export type AsObject = {
owner: string,
id: number,
}
}
export class UpdateBasicInfoRequest extends jspb.Message {
hasCharacter(): boolean;
clearCharacter(): void;
getCharacter(): CharacterIdentifier | undefined;
setCharacter(value?: CharacterIdentifier): void;
getName(): string;
setName(value: string): void;
getGender(): string;
setGender(value: string): void;
getConcept(): string;
setConcept(value: string): void;
getChronicle(): string;
setChronicle(value: string): void;
getAge(): number;
setAge(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateBasicInfoRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateBasicInfoRequest): UpdateBasicInfoRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UpdateBasicInfoRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UpdateBasicInfoRequest;
static deserializeBinaryFromReader(message: UpdateBasicInfoRequest, reader: jspb.BinaryReader): UpdateBasicInfoRequest;
}
export namespace UpdateBasicInfoRequest {
export type AsObject = {
character?: CharacterIdentifier.AsObject,
name: string,
gender: string,
concept: string,
chronicle: string,
age: number,
}
}
export class ApiResult extends jspb.Message {
getSuccess(): boolean;
setSuccess(value: boolean): void;
getError(): string;
setError(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ApiResult.AsObject;
static toObject(includeInstance: boolean, msg: ApiResult): ApiResult.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ApiResult, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ApiResult;
static deserializeBinaryFromReader(message: ApiResult, reader: jspb.BinaryReader): ApiResult;
}
export namespace ApiResult {
export type AsObject = {
success: boolean,
error: string,
}
}
export class UpdateAttributeRequest extends jspb.Message {
hasCharacter(): boolean;
clearCharacter(): void;
getCharacter(): CharacterIdentifier | undefined;
setCharacter(value?: CharacterIdentifier): void;
getAttributeName(): string;
setAttributeName(value: string): void;
getAttributeValue(): number;
setAttributeValue(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateAttributeRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateAttributeRequest): UpdateAttributeRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UpdateAttributeRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UpdateAttributeRequest;
static deserializeBinaryFromReader(message: UpdateAttributeRequest, reader: jspb.BinaryReader): UpdateAttributeRequest;
}
export namespace UpdateAttributeRequest {
export type AsObject = {
character?: CharacterIdentifier.AsObject,
attributeName: string,
attributeValue: number,
}
}
export class UpdateSkillRequest extends jspb.Message {
hasCharacter(): boolean;
clearCharacter(): void;
getCharacter(): CharacterIdentifier | undefined;
setCharacter(value?: CharacterIdentifier): void;
hasSkill(): boolean;
clearSkill(): void;
getSkill(): cofd_pb.CofdSheet.Skill | undefined;
setSkill(value?: cofd_pb.CofdSheet.Skill): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateSkillRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateSkillRequest): UpdateSkillRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UpdateSkillRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UpdateSkillRequest;
static deserializeBinaryFromReader(message: UpdateSkillRequest, reader: jspb.BinaryReader): UpdateSkillRequest;
}
export namespace UpdateSkillRequest {
export type AsObject = {
character?: CharacterIdentifier.AsObject,
skill?: cofd_pb.CofdSheet.Skill.AsObject,
}
}
export class UpdateSkillValueRequest extends jspb.Message {
hasCharacter(): boolean;
clearCharacter(): void;
getCharacter(): CharacterIdentifier | undefined;
setCharacter(value?: CharacterIdentifier): void;
getSkillName(): string;
setSkillName(value: string): void;
getSkillValue(): number;
setSkillValue(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateSkillValueRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateSkillValueRequest): UpdateSkillValueRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UpdateSkillValueRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UpdateSkillValueRequest;
static deserializeBinaryFromReader(message: UpdateSkillValueRequest, reader: jspb.BinaryReader): UpdateSkillValueRequest;
}
export namespace UpdateSkillValueRequest {
export type AsObject = {
character?: CharacterIdentifier.AsObject,
skillName: string,
skillValue: number,
}
}
export class UpdateSkillSpecializationsRequest extends jspb.Message {
getName(): string;
setName(value: string): void;
clearSpecializationsList(): void;
getSpecializationsList(): Array<string>;
setSpecializationsList(value: Array<string>): void;
addSpecializations(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateSkillSpecializationsRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateSkillSpecializationsRequest): UpdateSkillSpecializationsRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UpdateSkillSpecializationsRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UpdateSkillSpecializationsRequest;
static deserializeBinaryFromReader(message: UpdateSkillSpecializationsRequest, reader: jspb.BinaryReader): UpdateSkillSpecializationsRequest;
}
export namespace UpdateSkillSpecializationsRequest {
export type AsObject = {
name: string,
specializationsList: Array<string>,
}
}
export class AddConditionRequest extends jspb.Message {
getCharacterUsername(): string;
setCharacterUsername(value: string): void;
getCharacterId(): number;
setCharacterId(value: number): void;
getConditionName(): string;
setConditionName(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): AddConditionRequest.AsObject;
static toObject(includeInstance: boolean, msg: AddConditionRequest): AddConditionRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: AddConditionRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): AddConditionRequest;
static deserializeBinaryFromReader(message: AddConditionRequest, reader: jspb.BinaryReader): AddConditionRequest;
}
export namespace AddConditionRequest {
export type AsObject = {
characterUsername: string,
characterId: number,
conditionName: string,
}
}
export class RemoveConditionRequest extends jspb.Message {
getCharacterUsername(): string;
setCharacterUsername(value: string): void;
getCharacterId(): number;
setCharacterId(value: number): void;
getConditionName(): string;
setConditionName(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): RemoveConditionRequest.AsObject;
static toObject(includeInstance: boolean, msg: RemoveConditionRequest): RemoveConditionRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: RemoveConditionRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): RemoveConditionRequest;
static deserializeBinaryFromReader(message: RemoveConditionRequest, reader: jspb.BinaryReader): RemoveConditionRequest;
}
export namespace RemoveConditionRequest {
export type AsObject = {
characterUsername: string,
characterId: number,
conditionName: string,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
// package: models.proto.cofd.api
// file: cofd_api.proto

View File

@ -1,3 +0,0 @@
// package: models.proto.cofd.api
// file: cofd_api.proto

View File

@ -1,373 +0,0 @@
// package: models.proto.cofd
// file: cofd.proto
import * as jspb from "google-protobuf";
export class CoreFields extends jspb.Message {
getIntegrity(): number;
setIntegrity(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CoreFields.AsObject;
static toObject(includeInstance: boolean, msg: CoreFields): CoreFields.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: CoreFields, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): CoreFields;
static deserializeBinaryFromReader(message: CoreFields, reader: jspb.BinaryReader): CoreFields;
}
export namespace CoreFields {
export type AsObject = {
integrity: number,
}
}
export class MageFields extends jspb.Message {
getWidsom(): number;
setWidsom(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): MageFields.AsObject;
static toObject(includeInstance: boolean, msg: MageFields): MageFields.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: MageFields, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): MageFields;
static deserializeBinaryFromReader(message: MageFields, reader: jspb.BinaryReader): MageFields;
}
export namespace MageFields {
export type AsObject = {
widsom: number,
}
}
export class ChangelingFields extends jspb.Message {
getClarity(): number;
setClarity(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ChangelingFields.AsObject;
static toObject(includeInstance: boolean, msg: ChangelingFields): ChangelingFields.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ChangelingFields, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ChangelingFields;
static deserializeBinaryFromReader(message: ChangelingFields, reader: jspb.BinaryReader): ChangelingFields;
}
export namespace ChangelingFields {
export type AsObject = {
clarity: number,
}
}
export class CofdSheet extends jspb.Message {
getName(): string;
setName(value: string): void;
getGender(): string;
setGender(value: string): void;
getConcept(): string;
setConcept(value: string): void;
getAge(): number;
setAge(value: number): void;
getPlayer(): string;
setPlayer(value: string): void;
getChronicle(): string;
setChronicle(value: string): void;
getDescription(): string;
setDescription(value: string): void;
getStrength(): number;
setStrength(value: number): void;
getDexterity(): number;
setDexterity(value: number): void;
getStamina(): number;
setStamina(value: number): void;
getIntelligence(): number;
setIntelligence(value: number): void;
getWits(): number;
setWits(value: number): void;
getResolve(): number;
setResolve(value: number): void;
getPresence(): number;
setPresence(value: number): void;
getManipulation(): number;
setManipulation(value: number): void;
getComposure(): number;
setComposure(value: number): void;
getPhysicalSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
clearPhysicalSkillsMap(): void;
getMentalSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
clearMentalSkillsMap(): void;
getSocialSkillsMap(): jspb.Map<string, CofdSheet.Skill>;
clearSocialSkillsMap(): void;
clearMeritsList(): void;
getMeritsList(): Array<CofdSheet.Merit>;
setMeritsList(value: Array<CofdSheet.Merit>): void;
addMerits(value?: CofdSheet.Merit, index?: number): CofdSheet.Merit;
clearConditionsList(): void;
getConditionsList(): Array<CofdSheet.Condition>;
setConditionsList(value: Array<CofdSheet.Condition>): void;
addConditions(value?: CofdSheet.Condition, index?: number): CofdSheet.Condition;
getSize(): number;
setSize(value: number): void;
getHealth(): number;
setHealth(value: number): void;
getWillpower(): number;
setWillpower(value: number): void;
getExperiencePoints(): number;
setExperiencePoints(value: number): void;
getBeats(): number;
setBeats(value: number): void;
clearItemsList(): void;
getItemsList(): Array<CofdSheet.Item>;
setItemsList(value: Array<CofdSheet.Item>): void;
addItems(value?: CofdSheet.Item, index?: number): CofdSheet.Item;
clearAttacksList(): void;
getAttacksList(): Array<CofdSheet.Attack>;
setAttacksList(value: Array<CofdSheet.Attack>): void;
addAttacks(value?: CofdSheet.Attack, index?: number): CofdSheet.Attack;
getOtherDataMap(): jspb.Map<string, string>;
clearOtherDataMap(): void;
hasCore(): boolean;
clearCore(): void;
getCore(): CoreFields | undefined;
setCore(value?: CoreFields): void;
hasMage(): boolean;
clearMage(): void;
getMage(): MageFields | undefined;
setMage(value?: MageFields): void;
hasChangeling(): boolean;
clearChangeling(): void;
getChangeling(): ChangelingFields | undefined;
setChangeling(value?: ChangelingFields): void;
getSystemFieldsCase(): CofdSheet.SystemFieldsCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CofdSheet.AsObject;
static toObject(includeInstance: boolean, msg: CofdSheet): CofdSheet.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: CofdSheet, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): CofdSheet;
static deserializeBinaryFromReader(message: CofdSheet, reader: jspb.BinaryReader): CofdSheet;
}
export namespace CofdSheet {
export type AsObject = {
name: string,
gender: string,
concept: string,
age: number,
player: string,
chronicle: string,
description: string,
strength: number,
dexterity: number,
stamina: number,
intelligence: number,
wits: number,
resolve: number,
presence: number,
manipulation: number,
composure: number,
physicalSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
mentalSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
socialSkillsMap: Array<[string, CofdSheet.Skill.AsObject]>,
meritsList: Array<CofdSheet.Merit.AsObject>,
conditionsList: Array<CofdSheet.Condition.AsObject>,
size: number,
health: number,
willpower: number,
experiencePoints: number,
beats: number,
itemsList: Array<CofdSheet.Item.AsObject>,
attacksList: Array<CofdSheet.Attack.AsObject>,
otherDataMap: Array<[string, string]>,
core?: CoreFields.AsObject,
mage?: MageFields.AsObject,
changeling?: ChangelingFields.AsObject,
}
export class Merit extends jspb.Message {
getDots(): number;
setDots(value: number): void;
getName(): string;
setName(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Merit.AsObject;
static toObject(includeInstance: boolean, msg: Merit): Merit.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Merit, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Merit;
static deserializeBinaryFromReader(message: Merit, reader: jspb.BinaryReader): Merit;
}
export namespace Merit {
export type AsObject = {
dots: number,
name: string,
}
}
export class Condition extends jspb.Message {
getName(): string;
setName(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Condition.AsObject;
static toObject(includeInstance: boolean, msg: Condition): Condition.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Condition, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Condition;
static deserializeBinaryFromReader(message: Condition, reader: jspb.BinaryReader): Condition;
}
export namespace Condition {
export type AsObject = {
name: string,
}
}
export class Skill extends jspb.Message {
getDots(): number;
setDots(value: number): void;
getName(): string;
setName(value: string): void;
getUntrainedPenalty(): number;
setUntrainedPenalty(value: number): void;
clearSpecializationsList(): void;
getSpecializationsList(): Array<string>;
setSpecializationsList(value: Array<string>): void;
addSpecializations(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Skill.AsObject;
static toObject(includeInstance: boolean, msg: Skill): Skill.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Skill, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Skill;
static deserializeBinaryFromReader(message: Skill, reader: jspb.BinaryReader): Skill;
}
export namespace Skill {
export type AsObject = {
dots: number,
name: string,
untrainedPenalty: number,
specializationsList: Array<string>,
}
}
export class Item extends jspb.Message {
getName(): string;
setName(value: string): void;
getDescription(): string;
setDescription(value: string): void;
getRules(): string;
setRules(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Item.AsObject;
static toObject(includeInstance: boolean, msg: Item): Item.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Item, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Item;
static deserializeBinaryFromReader(message: Item, reader: jspb.BinaryReader): Item;
}
export namespace Item {
export type AsObject = {
name: string,
description: string,
rules: string,
}
}
export class Attack extends jspb.Message {
getName(): string;
setName(value: string): void;
getDicePool(): number;
setDicePool(value: number): void;
getDamage(): number;
setDamage(value: number): void;
getRange(): number;
setRange(value: number): void;
getInitiativeModifier(): number;
setInitiativeModifier(value: number): void;
getSize(): number;
setSize(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Attack.AsObject;
static toObject(includeInstance: boolean, msg: Attack): Attack.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Attack, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Attack;
static deserializeBinaryFromReader(message: Attack, reader: jspb.BinaryReader): Attack;
}
export namespace Attack {
export type AsObject = {
name: string,
dicePool: number,
damage: number,
range: number,
initiativeModifier: number,
size: number,
}
}
export enum SystemFieldsCase {
SYSTEM_FIELDS_NOT_SET = 0,
CORE = 30,
MAGE = 31,
CHANGELING = 32,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
// package: models.proto.cofd
// file: cofd.proto

View File

@ -1,3 +0,0 @@
// package: models.proto.cofd
// file: cofd.proto

View File

@ -1,34 +0,0 @@
import * as jspb from "google-protobuf";
import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest } from "../_proto/cofd_api_pb";
const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' };
function staticImplements<T>() {
return <U extends T>(constructor: U) => { constructor };
}
async function makeRequest<T extends jspb.Message>(uri: string, params: T): Promise<Uint8Array> {
let resp = await fetch(uri, {
method: 'POST',
headers: { ...PROTOBUF_CONTENT_TYPE },
body: params.serializeBinary()
});
const data = await resp.arrayBuffer();
return new Uint8Array(data);
}
export async function updateSkillValue(params: UpdateSkillValueRequest): Promise<ApiResult> {
let data = await makeRequest('/api/rpc/cofd/update_skill_value', params);
return ApiResult.deserializeBinary(data);
}
export async function updateAttributeValue(params: UpdateAttributeRequest): Promise<ApiResult> {
let data = await makeRequest('/api/rpc/cofd/update_attribute_value', params);
return ApiResult.deserializeBinary(data);
}
export async function updateBasicInfo(params: UpdateBasicInfoRequest): Promise<ApiResult> {
let data = await makeRequest('/api/rpc/cofd/update_basic_info', params);
return ApiResult.deserializeBinary(data);
}

View File

@ -1,102 +0,0 @@
import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest } from "../../_proto/cofd_api_pb";
import * as api from "../api";
// This is the scripting for the edit character page, which submits
// changes to the server as the user makes them.
(async () => {
// Useful for making sure elements actually exist in event handler.
type Option<T> = T | null | undefined;
const [, , USERNAME, CHARACTER_ID] = window.location.pathname.split('/');
function characterId(): CharacterIdentifier {
const id = new CharacterIdentifier();
id.setId(parseInt(CHARACTER_ID));
id.setOwner(USERNAME);
return id;
}
const getTextValue = (selector: string) =>
document.querySelector<HTMLInputElement>(selector)?.value ?? "";
const getIntValue = (selector: string) =>
parseInt(document.querySelector<HTMLInputElement>(selector)?.value ?? "0")
function setupAttributes() {
const attributeInputs = document.querySelectorAll('#attributes input[type="number"]');
async function attributeHandler(event: Event) {
const input = event.target as Option<HTMLInputElement>;
if (!input) return;
console.log("updating attr");
const attribute = input.id;
const newValue = parseInt(input.value);
const params = new UpdateAttributeRequest();
params.setCharacter(characterId());
params.setAttributeName(attribute);
params.setAttributeValue(newValue);
let resp = await api.updateAttributeValue(params);
console.log("got a response back", resp);
}
Array.from(attributeInputs).forEach(input => {
input.addEventListener('change', attributeHandler);
});
}
function setupSkills() {
const skillInputs = document.querySelectorAll('#skills input[type="number"]');
async function skillValueHandler(event: Event) {
const input = event.target as Option<HTMLInputElement>;
if (!input) return;
console.log("updating skill");
const attribute = input.id;
const newValue = parseInt(input.value);
const params = new UpdateSkillValueRequest();
params.setCharacter(characterId());
params.setSkillName(attribute);
params.setSkillValue(newValue);
let resp = await api.updateSkillValue(params);
console.log("got a response back", resp);
}
Array.from(skillInputs).forEach(input => {
input.addEventListener('change', skillValueHandler);
});
}
function setupBasicInfo() {
async function updateInfo() {
const params = new UpdateBasicInfoRequest();
params.setCharacter(characterId());
params.setName(getTextValue("#characterName"));
params.setAge(getIntValue("#age"));
params.setConcept(getTextValue("#concept"));
params.setChronicle(getTextValue("#chronicle"));
params.setGender(getTextValue("#gender"));
let resp = await api.updateBasicInfo(params);
console.log("got a response back", resp);
}
const inputs = document.querySelectorAll("#basicInfo input");
inputs.forEach(input => {
console.log('got an input', input);
input.addEventListener('blur', updateInfo);
});
}
setupAttributes();
setupSkills();
setupBasicInfo();
})().catch(e => {
alert(e);
});

View File

@ -1,148 +0,0 @@
{% import "characters/view_character_macros" as macros %}
{% extends "base" %}
{% block content %}
<style type="text/css">
body {
font-family: Liberation Sans, Arial;
}
#attributes {
padding: 4px;
border-collapse: collapse;
}
#attributes .attributes-section {
border: 1px solid gray;
border-collapse: collapse;
display: table-cell;
}
.attribute {
margin: 0;
padding: 0;
display: flex;
}
.attribute label {
display: inline-block;
float: left;
clear: left;
width: 10em;
text-align: right;
vertical-align: text-bottom;
padding: 8px;
margin: 0;
}
.attribute div.value {
min-width: 1.5em;
max-width: 4em;
text-align: center;
display: inline-block;
float: left;
padding: 8px;
border: none;
background-color: lightgray;
margin: 0;
}
#skills {
padding: 4px;
border-collapse: collapse;
}
#skills .skills-section {
border: 1px solid gray;
border-collapse: collapse;
display: table-cell;
}
.skill {
margin: 0;
padding: 0;
display: flex;
}
.skill label {
display: inline-block;
float: left;
clear: left;
width: 10em;
text-align: right;
vertical-align: text-bottom;
padding: 8px;
margin: 0;
}
.skill div.value {
min-width: 1.5em;
max-width: 4em;
display: inline-block;
text-align: center;
float: left;
padding: 8px;
border: none;
background-color: lightgray;
margin: 0;
}
</style>
<h1>Core Sheet</h1>
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>
<p>System: {{data_type}}</h3>
</div>
<div>
<h1>Core Sheet</h1>
<div>
<h1>Name: <input type="text" value="{{name}}" /></h1>
<p>System: {{data_type}}</p>
<div id="attributes">
<div class="attributes-section" id="mentalAttributes">
{{ macros::attribute(name="Intelligence", value=sheet.intelligence) }}
{{ macros::attribute(name="Wits", value=sheet.wits) }}
{{ macros::attribute(name="Resolve", value=sheet.resolve) }}
</div>
<div class="attributes-section" id="physicalAttributes">
{{ macros::attribute(name="Strength", value=sheet.strength) }}
{{ macros::attribute(name="Dexterity", value=sheet.dexterity) }}
{{ macros::attribute(name="Stamina", value=sheet.stamina) }}
</div>
<div class="attributes-section" id="socicalAttributes">
{{ macros::attribute(name="Presence", value=sheet.presence) }}
{{ macros::attribute(name="Manipulation", value=sheet.manipulation) }}
{{ macros::attribute(name="Composure", value=sheet.composure) }}
</div>
</div>
<div id="skills">
<div class="skills-section" id="mentalSkills">
{% for skill_name, skill in sheet.mentalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="physicalSkills">
{% for skill_name, skill in sheet.physicalSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
<div class="skills-section" id="socialSkills">
{% for skill_name, skill in sheet.socialSkills %}
{{ macros::skill(name=skill_name, value=skill.dots) }}
{% endfor %}
</div>
</div>
</div>
<div>
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
</div>
{% endblock content %}

View File

@ -1,13 +0,0 @@
{% macro attribute(name, value) %}
<div class="attribute">
<label for="{{name}}">{{name}}:</label>
<div class="value" id="{{name}}">{{value}}</div>
</div>
{% endmacro attribute %}
{% macro skill(name, value) %}
<div class="skill">
<label for="{{name}}">{{name}}:</label>
<div class="value" id="{{name}}">{{value}}</div>
</div>
{% endmacro skill %}

View File

@ -4,11 +4,14 @@ extern crate rocket;
#[macro_use] #[macro_use]
extern crate rocket_contrib; extern crate rocket_contrib;
use log::{error, info}; // Seemingly necessary to get serde::Serialize into scope for Prost
// code generation.
#[macro_use]
extern crate serde_derive;
use rocket_contrib::serve::StaticFiles; use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::env; use std::env;
use tonic::transport::Server;
pub mod catchers; pub mod catchers;
pub mod db; pub mod db;
@ -17,40 +20,6 @@ pub mod migrator;
pub mod models; pub mod models;
pub mod routes; pub mod routes;
async fn make_rocket(database: sqlx::SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
info!("Running Rocket");
let root_routes: Vec<rocket::Route> = {
routes::root::routes()
.into_iter()
.chain(routes::auth::routes().into_iter())
.collect()
};
let api_routes = routes::api::routes();
let character_routes = routes::characters::routes();
let catchers = catchers::catchers();
rocket::ignite()
.attach(Template::fairing())
.manage(database)
.mount("/", root_routes)
.mount("/characters", character_routes)
.mount("/api", api_routes)
.mount(
"/scripts",
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/scripts")),
)
.mount(
"/protos",
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
)
.register(catchers)
.launch()
.await?;
Ok(())
}
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
@ -64,7 +33,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
migrator::migrate(db_path).await?; migrator::migrate(db_path).await?;
let db = crate::db::create_pool(db_path).await?; let root_routes: Vec<rocket::Route> = {
routes::root::routes()
.into_iter()
.chain(routes::auth::routes().into_iter())
.collect()
};
make_rocket(db.clone()).await let api_routes = routes::api::routes();
let character_routes = routes::characters::routes();
let catchers = catchers::catchers();
rocket::ignite()
.attach(Template::fairing())
//.attach(db::TenebrousDbConn::fairing())
.manage(crate::db::create_pool(db_path).await?)
.mount("/", root_routes)
.mount("/characters", character_routes)
.mount("/api", api_routes)
.mount(
"/scripts",
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static/scripts")),
)
.mount(
"/protos",
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
)
.register(catchers)
.launch()
.await
.map_err(|e| e.into())
} }

View File

@ -2,7 +2,7 @@ use crate::errors::Error;
use crate::models::proto::cofd::*; use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use prost::bytes::BytesMut; use prost::bytes::BytesMut;
use serde::Serialize; use serde_derive::Serialize;
use strum::{EnumIter, EnumString}; use strum::{EnumIter, EnumString};
/// Dynamic character data is an opaque container type that holds /// Dynamic character data is an opaque container type that holds
@ -146,8 +146,8 @@ impl Character {
} }
/// Update the existing character with new serialized protobuf /// Update the existing character with new serialized protobuf
/// data. /// data. Consumes the data.
pub fn update_data<T>(&mut self, data: &T) -> Result<(), Error> pub fn update_data<T>(&mut self, data: T) -> Result<(), Error>
where where
T: prost::Message + std::default::Default, T: prost::Message + std::default::Default,
{ {

View File

@ -1,7 +1,6 @@
use super::characters::CharacterDataType; use super::characters::CharacterDataType;
use rocket::http::RawStr; use rocket::http::RawStr;
use rocket::request::FromFormValue; use rocket::request::FromFormValue;
use serde::Serialize;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; use thiserror::Error;

View File

@ -1,21 +1,25 @@
use crate::errors::Error; use crate::errors::Error;
use prost::bytes::BytesMut;
use rocket::data::{Data, FromData, Outcome, ToByteUnit}; use rocket::data::{Data, FromData, Outcome, ToByteUnit};
use rocket::http::{ContentType, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::status;
use rocket::response::{self, Responder, Response};
use std::default::Default; use std::default::Default;
use std::io::Cursor; use std::ops::Deref;
use std::ops::{Deref, DerefMut};
pub mod cofd; pub mod cofd;
const CRATE_NAME: &'static str = env!("CARGO_BIN_NAME");
/// Convert an incoming protobuf content-type to the equivalent type
/// name produced by std::any::type_name(). Currently does NOT work
/// with nested types due to how prost generates the module names.
fn convert_to_rust_name(proto_type: &str) -> String {
format!("{}::{}", CRATE_NAME, proto_type.replace(".", "::"))
}
/// A struct wrapping a protobuf that allows it to be used as binary /// A struct wrapping a protobuf that allows it to be used as binary
/// data submitted via POST using fetch API. Can automatically be /// data submitted via POST using fetch API. Can automatically be
/// dereferenced into its wrapped type. /// dereferenced into its wrapped type.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Proto<T>(pub T) pub(crate) struct Proto<T>(T)
where where
T: prost::Message + Default; T: prost::Message + Default;
@ -36,10 +40,21 @@ where
.map(|ct| ct.top() == "application" && ct.sub() == "x-protobuf") .map(|ct| ct.top() == "application" && ct.sub() == "x-protobuf")
.unwrap_or(false); .unwrap_or(false);
let message_type: Option<String> = content_type.and_then(|ct| {
ct.params()
.find(|&(name, _)| name == "messageType")
.map(|(_, message_type)| convert_to_rust_name(message_type))
});
if !is_protobuf { if !is_protobuf {
return Outcome::Failure((Status::new(422, "invalid protobuf"), Error::InvalidInput)); return Outcome::Failure((Status::new(422, "invalid protobuf"), Error::InvalidInput));
} }
if message_type.as_ref().map(String::as_str) != Some(std::any::type_name::<T>()) {
println!("message type is {:?}", message_type);
return Outcome::Forward(data);
}
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await { let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
Ok(read_bytes) => read_bytes, Ok(read_bytes) => read_bytes,
Err(e) => return Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())), Err(e) => return Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())),
@ -52,22 +67,6 @@ where
} }
} }
impl<'r, T> Responder<'r, 'static> for Proto<T>
where
T: prost::Message + Default,
{
fn respond_to(self, req: &Request) -> response::Result<'static> {
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&self.0));
match self.0.encode(&mut buf) {
Ok(_) => Response::build()
.header(ContentType::new("application", "x-protobuf"))
.sized_body(buf.len(), Cursor::new(buf))
.ok(),
Err(e) => status::Custom(Status::InternalServerError, e.to_string()).respond_to(req),
}
}
}
/// Enable automatically calling methods on a decoded Proto instance. /// Enable automatically calling methods on a decoded Proto instance.
impl<T> Deref for Proto<T> impl<T> Deref for Proto<T>
where where
@ -79,12 +78,3 @@ where
&self.0 &self.0
} }
} }
impl<T> DerefMut for Proto<T>
where
T: prost::Message + Default,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -6,50 +6,11 @@ use crate::models::characters::CharacterDataType;
use std::collections::BTreeMap; use std::collections::BTreeMap;
//Add the generated protobuf code into this module. //Add the generated protobuf code into this module.
//include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs")); include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs"));
tonic::include_proto!("models.proto.cofd");
//Add the API protobuf genreated code for the api module. //Add the API protobuf genreated code for the api module.
pub mod api { pub mod api {
//include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs")); include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs"));
tonic::include_proto!("models.proto.cofd.api");
use std::borrow::Cow;
/// Trait to extract values out of a CharacterIdentifier whose
/// instance may or may not exist on an API request.
pub trait DefaultCharacterIdentifier {
/// Retrieve the specified owner, or a default fallback (empty
/// string). Will not allocate if the owner is present in the
/// request.
fn owner(&self) -> Cow<'_, str>;
/// Retrieve the character ID specified, or the default i32
/// value (0).
fn id(&self) -> i32;
}
impl DefaultCharacterIdentifier for Option<CharacterIdentifier> {
fn owner(&self) -> Cow<'_, str> {
self.as_ref()
.map(|ident| Cow::from(&ident.owner))
.unwrap_or_default()
}
fn id(&self) -> i32 {
self.as_ref().map(|ident| ident.id).unwrap_or_default()
}
}
/// Helpers for the ApiResult class.
impl ApiResult {
pub fn success() -> Self {
ApiResult {
success: true,
error: "".to_string(),
}
}
}
} }
/// Default mental skill names for a regular Chronicles of Darkness /// Default mental skill names for a regular Chronicles of Darkness

View File

@ -3,7 +3,7 @@ use argon2::{self, Config, Error as ArgonError};
use rand::Rng; use rand::Rng;
use rocket::outcome::IntoOutcome; use rocket::outcome::IntoOutcome;
use rocket::request::{self, FromRequest, Request}; use rocket::request::{self, FromRequest, Request};
use serde::Serialize; use serde_derive::Serialize;
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> { pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
let salt = rand::thread_rng().gen::<[u8; 16]>(); let salt = rand::thread_rng().gen::<[u8; 16]>();

View File

@ -1,15 +1,19 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::characters::Character; use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use rocket_contrib::templates::Template;
mod cofd; use serde::Serialize;
use std::borrow::Cow;
use std::collections::btree_map::{Entry, OccupiedEntry};
pub(crate) fn routes() -> Vec<rocket::Route> { pub(crate) fn routes() -> Vec<rocket::Route> {
routes![ routes![
cofd::update_basic_info, cofd::update_basic_info,
cofd::update_attribute_value, cofd::update_attributes,
cofd::update_skill, cofd::update_attribute,
cofd::update_skills,
cofd::update_skill_value, cofd::update_skill_value,
cofd::add_condition, cofd::add_condition,
cofd::remove_condition cofd::remove_condition
@ -39,3 +43,169 @@ async fn load_character(
Ok(character) Ok(character)
} }
/// Protobuf-based REST endpoints for editing a character.
mod cofd {
use super::*;
use crate::models::proto::cofd::cofd_sheet::Skill;
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
fn find_skill_entry<'a>(
sheet: &'a mut CofdSheet,
skill_name: &'a str,
) -> Option<OccupiedEntry<'a, String, Skill>> {
let all_skills = vec![
&mut sheet.mental_skills,
&mut sheet.physical_skills,
&mut sheet.social_skills,
];
// Search all skill lists for this value using "workaround" to
// break value from for loops.
let skill: Option<OccupiedEntry<_, _>> = 'l: loop {
for skill_map in all_skills {
if let Entry::Occupied(entry) = skill_map.entry(skill_name.to_owned()) {
break 'l Some(entry);
}
}
break None;
};
skill
}
fn find_skill<'a>(sheet: &'a mut CofdSheet, skill_name: &'a str) -> Option<&'a mut Skill> {
find_skill_entry(sheet, skill_name).map(|entry| entry.into_mut())
}
#[post("/cofd/<owner>/<character_id>/basic-info", data = "<info>")]
pub(super) fn update_basic_info<'a>(
owner: String,
character_id: i32,
info: Proto<BasicInfo>,
) -> &'a str {
"lol"
}
#[post("/cofd/<owner>/<character_id>/attributes", data = "<info>")]
pub(super) fn update_attributes<'a>(
owner: String,
character_id: i32,
info: Proto<Attributes>,
) -> &'a str {
"lol"
}
#[post("/rpc/cofd/update_attribute", data = "<req>")]
pub(super) async fn update_attribute<'a>(
req: Proto<UpdateAttributeRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<&'a str, Error> {
let mut character = load_character(
&conn,
logged_in_user,
&req.character_username,
req.character_id,
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let value = req.attribute_value;
match req.attribute_name.to_lowercase().as_ref() {
"strength" => Ok(sheet.strength = value),
"dexterity" => Ok(sheet.dexterity = value),
"stamina" => Ok(sheet.stamina = value),
"intelligence" => Ok(sheet.intelligence = value),
"wits" => Ok(sheet.wits = value),
"resolve" => Ok(sheet.resolve = value),
"presence" => Ok(sheet.presence = value),
"manipulation" => Ok(sheet.manipulation = value),
"composure" => Ok(sheet.composure = value),
_ => Err(Error::InvalidInput),
}?;
character.update_data(sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[patch(
"/cofd/<owner>/<character_id>/skills",
data = "<skill_update>",
rank = 1
)]
pub(super) async fn update_skills<'a>(
owner: String,
character_id: i32,
skill_update: Proto<SkillUpdate>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<&'a str, Error> {
let mut character = load_character(&conn, logged_in_user, &owner, character_id).await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let updated_skill: &Skill = skill_update.skill.as_ref().ok_or(Error::InvalidInput)?;
let skill_entry = find_skill_entry(&mut sheet, &skill_update.name);
skill_entry
.map(|mut entry| entry.insert(updated_skill.clone()))
.ok_or(Error::InvalidInput)?;
println!(
"updated skill {} with {:?}",
skill_update.name, skill_update.skill
);
character.update_data(sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[post("/rpc/cofd/update_skill_value", data = "<request>")]
pub(super) async fn update_skill_value<'a>(
request: Proto<UpdateSkillValueRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<&'a str, Error> {
println!("{:#?}", request);
let mut character = load_character(
&conn,
logged_in_user,
&request.character_username,
request.character_id,
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let skill: Option<&mut Skill> = find_skill(&mut sheet, &request.skill_name);
skill
.map(|s| s.dots = request.skill_value)
.ok_or(Error::InvalidInput)?;
println!("updated skill value",);
character.update_data(sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
pub(super) fn add_condition<'a>(
owner: String,
character_id: i32,
info: Proto<Condition>,
) -> &'a str {
"lol"
}
#[delete("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
pub(super) fn remove_condition<'a>(
owner: String,
character_id: i32,
info: Proto<Condition>,
) -> &'a str {
"lol"
}
}

View File

@ -1,174 +0,0 @@
use super::load_character;
use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error;
use crate::models::characters::Character;
use crate::models::proto::cofd::api::DefaultCharacterIdentifier;
use crate::models::proto::cofd::cofd_sheet::Skill;
use crate::models::proto::cofd::*;
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
use crate::models::users::User;
use std::collections::btree_map::{Entry, OccupiedEntry};
fn find_skill_entry<'a>(
sheet: &'a mut CofdSheet,
skill_name: &'a str,
) -> Option<OccupiedEntry<'a, String, Skill>> {
let all_skills = vec![
&mut sheet.mental_skills,
&mut sheet.physical_skills,
&mut sheet.social_skills,
];
// Search all skill lists for this value using "workaround" to
// break value from for loops.
let skill: Option<OccupiedEntry<_, _>> = 'l: loop {
for skill_map in all_skills {
if let Entry::Occupied(entry) = skill_map.entry(skill_name.to_owned()) {
break 'l Some(entry);
}
}
break None;
};
skill
}
fn find_skill<'a>(sheet: &'a mut CofdSheet, skill_name: &'a str) -> Option<&'a mut Skill> {
find_skill_entry(sheet, skill_name).map(|entry| entry.into_mut())
}
#[post("/rpc/cofd/update_basic_info", data = "<req>")]
pub(super) async fn update_basic_info<'a>(
req: Proto<UpdateBasicInfoRequest>,
conn: TenebrousDbConn<'_>,
user: Option<&User>,
) -> Result<Proto<ApiResult>, Error> {
let mut character =
load_character(&conn, user, &req.character.owner(), req.character.id()).await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
println!("name will now be {}", req.name);
character.character_name = req.name.clone(); //Should probably remove name from the sheet?
sheet.name = req.name.clone();
sheet.gender = req.gender.clone();
sheet.concept = req.concept.clone();
sheet.chronicle = req.chronicle.clone();
sheet.age = req.age;
character.update_data(&sheet)?;
conn.update_character(&character).await?;
println!("Updated basic info");
Ok(Proto(ApiResult::success()))
}
#[post("/rpc/cofd/update_attribute_value", data = "<req>")]
pub(super) async fn update_attribute_value(
req: Proto<UpdateAttributeRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<Proto<ApiResult>, Error> {
let mut character = load_character(
&conn,
logged_in_user,
&req.character.owner(),
req.character.id(),
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let value = req.attribute_value;
match req.attribute_name.to_lowercase().as_ref() {
"strength" => Ok(sheet.strength = value),
"dexterity" => Ok(sheet.dexterity = value),
"stamina" => Ok(sheet.stamina = value),
"intelligence" => Ok(sheet.intelligence = value),
"wits" => Ok(sheet.wits = value),
"resolve" => Ok(sheet.resolve = value),
"presence" => Ok(sheet.presence = value),
"manipulation" => Ok(sheet.manipulation = value),
"composure" => Ok(sheet.composure = value),
_ => Err(Error::InvalidInput),
}?;
character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?;
Ok(Proto(ApiResult {
success: true,
error: "".to_string(),
}))
}
#[patch("/rpc/cofd/update_skill", data = "<skill_update>")]
pub(super) async fn update_skill<'a>(
skill_update: Proto<UpdateSkillRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<&'a str, Error> {
let mut character = load_character(
&conn,
logged_in_user,
&skill_update.character.owner(),
skill_update.character.id(),
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let updated_skill: &Skill = skill_update.skill.as_ref().ok_or(Error::InvalidInput)?;
let skill_entry = find_skill_entry(&mut sheet, &updated_skill.name);
skill_entry
.map(|mut entry| entry.insert(updated_skill.clone()))
.ok_or(Error::InvalidInput)?;
println!(
"updated skill {} with {:?}",
updated_skill.name, skill_update.skill
);
character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[post("/rpc/cofd/update_skill_value", data = "<req>")]
pub(super) async fn update_skill_value<'a>(
req: Proto<UpdateSkillValueRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<Proto<ApiResult>, Error> {
let mut character = load_character(
&conn,
logged_in_user,
&req.character.owner(),
req.character.id(),
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let mut skill: Option<&mut Skill> = find_skill(&mut sheet, &req.skill_name);
if let Some(ref mut s) = skill {
s.dots = req.skill_value;
}
println!("updated skill value");
character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?;
Ok(Proto(ApiResult {
success: true,
error: "".to_string(),
}))
}
#[put("/rpc/cofd/add_condition", data = "<info>")]
pub(super) fn add_condition<'a>(info: Proto<AddConditionRequest>) -> &'a str {
"lol"
}
#[delete("/rpc/cofd/remove_condition", data = "<info>")]
pub(super) fn remove_condition<'a>(info: Proto<RemoveConditionRequest>) -> &'a str {
"lol"
}

View File

@ -3,7 +3,6 @@ use crate::errors::Error;
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility}; use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
use crate::models::users::User; use crate::models::users::User;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use serde::Serialize;
mod edit; mod edit;
mod new; mod new;

View File

@ -7,7 +7,6 @@ use crate::models::{
}; };
use rocket::{request::Form, response::Redirect}; use rocket::{request::Form, response::Redirect};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use serde::Serialize;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
/// Form submission for creating a new character. /// Form submission for creating a new character.

View File

@ -4,7 +4,7 @@ use crate::models::characters::Visibility;
use crate::models::{characters::StrippedCharacter, users::User}; use crate::models::{characters::StrippedCharacter, users::User};
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use serde::Serialize; use serde_derive::Serialize;
pub fn routes() -> Vec<rocket::Route> { pub fn routes() -> Vec<rocket::Route> {
routes![index, user_index, proto_test] routes![index, user_index, proto_test]

68
static/scripts/api.js Normal file
View File

@ -0,0 +1,68 @@
function makeAPI(root) {
//Protobuf types
const UpdateAttributeRequestType = 'models.proto.cofd.api.UpdateAttributeRequest';
const UpdateAttributeRequest = root.lookupType(UpdateAttributeRequestType);
const UpdateSkillValueRequestType = 'models.proto.cofd.api.UpdateSkillValueRequest';
const UpdateSkillValueRequest = root.lookupType(UpdateSkillValueRequestType);
//TODO rpc-ify
const SkillSpecializationUpdateType = 'models.proto.cofd.api.SkillSpecializationsUpdate';
const SkillSpecializationsUpdate = root.lookupType(SkillSpecializationUpdateType);
const protobufContentType = (messageType) =>
({ 'Content-Type': 'application/x-protobuf; messageType="' + messageType + '"' });
function verifyAndCreate(protobufType, payload) {
let err = protobufType.verify(payload);
if (err) throw err;
return protobufType.create(payload);
}
async function updateAttribute(params) {
const { username, characterID, attribute, newValue } = params;
let req = verifyAndCreate(UpdateAttributeRequest, {
characterUsername: username,
characterId: parseInt(characterID),
attributeName: attribute,
attributeValue: parseInt(newValue)
});
let resp = await fetch('/api/rpc/cofd/update_attribute', {
method: 'POST',
headers: { ... protobufContentType(UpdateAttributeRequestType) },
body: UpdateAttributeRequest.encode(req).finish()
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
console.log("err is", err.text());
});
}
async function updateSkillValue(params) {
const { username, characterID, skillName, newValue } = params;
let req = verifyAndCreate(UpdateSkillValueRequest, {
characterUsername: username,
characterId: parseInt(characterID),
skillName: skillName,
skillValue: parseInt(newValue)
});
let resp = await fetch('/api/rpc/cofd/update_skill_value', {
method: 'POST',
headers: { ... protobufContentType(UpdateSkillValueRequestType) },
body: UpdateSkillValueRequest.encode(req).finish()
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
console.log("err is", err.text());
});
}
return {
updateAttribute,
updateSkillValue
};
}

View File

@ -0,0 +1,48 @@
(async () => {
//TODO start refactoring these into a separate script, and make API calls
//take all necessary info (e.g. username and character ID, plus other stuff)
//as object params.
const root = await protobuf.load("/protos/cofd_api.proto");
const [, , USERNAME, CHARACTER_ID] = window.location.pathname.split('/');
const api = makeAPI(root);
console.log("api is", api);
function setupAttributes() {
const attributeInputs = document.querySelectorAll('#attributes input[type="number"]');
async function attributeHandler(event) {
console.log("updating attr");
const attribute = event.target.id;
const newValue = parseInt(event.target.value);
const params = { username: USERNAME, characterID: CHARACTER_ID, attribute, newValue };
await api.updateAttribute(params);
}
Array.from(attributeInputs).forEach(input => {
input.addEventListener('change', attributeHandler);
});
}
function setupSkills() {
const attributeInputs = document.querySelectorAll('#skills input[type="number"]');
async function skillValueHandler(event) {
console.log("updating skill value");
const skillName = event.target.id;
const newValue = parseInt(event.target.value);
const params = { username: USERNAME, characterID: CHARACTER_ID, skillName, newValue };
await api.updateSkillValue(params);
}
Array.from(attributeInputs).forEach(input => {
input.addEventListener('change', skillValueHandler);
});
}
setupAttributes();
setupSkills();
})().catch(e => {
alert(e);
});

View File

@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', event => {
protobuf.load("/protos/cofd.proto").then(function(root) {
console.log("root is", root);
let CofdSheet = root.lookupType("models.proto.cofd.CofdSheet");
let sheet = CofdSheet.fromObject({ name: 'lol', strength: 100 });
let buffer = CofdSheet.encode(sheet).finish();
fetch('/proto-test', {
method: 'POST',
body: buffer
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
console.log("err is", err.text());
});
});
});

View File

@ -84,39 +84,13 @@
} }
</style> </style>
{# Webpack Templating for API script #} <script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
<%= htmlWebpackPlugin.tags.bodyTags %> <script defer type="text/javascript" src="/scripts/api.js"></script>
<script defer type="text/javascript" src="/scripts/characters/edit.js"></script>
<h1>Core Sheet</h1> <h1>Core Sheet</h1>
<div> <div>
<div id="basicInfo"> <h1>Name: <input type="text" value="{{name}}" /></h1>
<h1> <p>System: {{data_type}}</p>
<label for="characterName">Name:</label>
<input type="text" id="characterName" name="characterName" value="{{name}}" />
</h1>
<div>System: {{data_type}}</div>
<div>
<label for="gender">Gender:</label>
<input type="text" id="gender" name="gender" value="{{sheet.gender}}" />
</div>
<div>
<label for="age">Age:</label>
<input type="number" id="age" name="age" min="0" value="{{sheet.age}}" />
</div>
<div>
<label for="concept">Concept:</label>
<input type="text" id="concept" name="concept" value="{{sheet.concept}}" />
</div>
<div>
<label for="chronicle">Chronicle:</label>
<input type="text" id="chronicle" name="chronicle" value="{{sheet.chronicle}}" />
</div>
</div>
<div id="attributes"> <div id="attributes">
<div class="attributes-section" id="mentalAttributes"> <div class="attributes-section" id="mentalAttributes">
{{ macros::attribute(name="Intelligence", value=sheet.intelligence) }} {{ macros::attribute(name="Intelligence", value=sheet.intelligence) }}

View File

@ -1,6 +1,8 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
<script type="text/javascript" src="/scripts/characters/new-character.js"></script>
<div> <div>
New character page. New character page.

View File

@ -0,0 +1,15 @@
{% extends "base" %}
{% block content %}
<h1>Core Sheet</h1>
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>
<p>System: {{data_type}}</h3>
<p>Strength: {{sheet.strength}}</p>
</div>
<div>
<a href="/characters/{{username}}/{{id}}/edit">Edit Character</a>
</div>
{% endblock content %}

View File

@ -1,7 +1,6 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<%= htmlWebpackPlugin.tags.bodyTags %>
<div> <div>
<h1>Tenebrous: Login</h1> <h1>Tenebrous: Login</h1>

View File

@ -1,17 +0,0 @@
{
"compilerOptions": {
"alwaysStrict": true,
"sourceMap": true,
"target": "es5",
"removeComments": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"strictNullChecks": true,
"stripInternal": true,
"noFallthroughCasesInSwitch": true,
"noEmitOnError": true,
"lib": [
"dom"
]
}
}

View File

@ -1,67 +0,0 @@
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const root = path.resolve(__dirname, '.');
function packPage(page, chunks) {
if (!chunks) chunks = [];
return new HtmlWebpackPlugin({
template: `${root}/src/frontend/templates/${page}`,
filename: `${root}/generated/templates/${page}`,
publicPath: '/',
scriptLoading: 'defer',
chunks: chunks,
inject: false,
minify: false
});
}
module.exports = {
entry: {
edit_character: "./src/frontend/scripts/characters/edit.ts",
},
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: 'all',
},
},
mode: "development",
output: {
path: `${root}/generated`,
filename: 'scripts/dist/[name].bundle.js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.ts$/,
include: /src|_proto/,
exclude: /node_modules/,
loader: "ts-loader"
}
]
},
resolve: {
extensions: [".ts", ".js"]
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['!templates/**/*'],
cleanAfterEveryBuildPatterns: ['!templates/**/*'],
dry: false
}),
packPage('login.html.tera'),
packPage('base.html.tera'),
packPage('error.html.tera'),
packPage('index.html.tera'),
packPage('registration.html.tera'),
packPage('characters/edit_character.html.tera', ['edit_character']),
packPage('characters/edit_character_macros.html.tera'),
packPage('characters/new_character.html.tera'),
packPage('characters/view_character.html.tera'),
packPage('characters/view_character_macros.html.tera'),
]
};