Compare commits

...

32 Commits

Author SHA1 Message Date
jeff 2738de85db Remove extern crate for serde serialize 2021-01-15 20:34:06 +00:00
jeff 92c402e254 Switch all existing API requests to use new CharacterIdentifier stuff. 2021-01-12 22:01:17 +00:00
jeff 69d9d7ff80 Use Cow in favor of take. 2021-01-12 20:44:26 +00:00
jeff 6c1d19aa44 Rename 'or_default' to 'extract'. Better meaning. 2021-01-10 20:54:05 +00:00
jeff 5ea5d627bb Implement rest of basic info editing. 2021-01-10 20:45:15 +00:00
jeff ae6d5bcfa3 Implement updating basic info for character name. 2021-01-10 20:45:15 +00:00
jeff ffe4f9298c Create "character identifier" type for use in all API requests.
Will replace the character_username and character_id values in all
relevant requests.
2021-01-10 20:45:15 +00:00
jeff ea4723cbe5 Add API endpoint for updating basic character info. 2021-01-10 13:56:44 +00:00
jeff c42f13af52 Remove unused REST semantics from cofd API. 2021-01-09 20:02:24 +00:00
jeff e6845d8c7a Fix webpack publicPath to match server route. 2021-01-09 19:41:57 +00:00
jeff 9ca1f989c5 Move cofd api endpoints into their own file 2021-01-09 19:39:35 +00:00
jeff 31091fcb8e Update license in package.json to AGPL 2021-01-09 19:29:47 +00:00
jeff d6944cd4c1 Adjust webpack config slightly for better generation. 2021-01-09 13:32:08 +00:00
jeff f249ab2674 Rename static directory to generated 2021-01-09 13:03:07 +00:00
jeff bf75b84f3d Remove old comments in edit character script. 2021-01-09 12:59:11 +00:00
jeff 06dc6c1274 Move typescript into source tree. 2021-01-09 12:57:13 +00:00
jeff 133b4e6236 Implement a bit more of the view character template. 2021-01-05 21:42:38 +00:00
jeff 2008b90cba Remove more unneeded things 2021-01-05 21:33:21 +00:00
jeff 3fc9378f50 Clean up files and remove things that no longer need to exist. 2021-01-05 21:25:26 +00:00
jeff dd2741a0c1 Return semi-proper values from the protobuf HTTP API 2021-01-05 21:19:47 +00:00
jeff dcbf801ecc Reimplement attribute updtaes 2021-01-05 19:32:03 +00:00
jeff af01a56907 WIP on reimplementing edit character script with typescript 2021-01-04 22:36:52 +00:00
jeff 06262e6ad3 Compiles, assuming the user id will be in grpc metadata from auth interceptor. 2021-01-03 23:18:36 +00:00
jeff 592c925bc1 WIP - at a crossroads where we need some kind of auth mechanism 2021-01-03 22:05:28 +00:00
jeff f479da9001 Add readme instructions, run proxy command. 2021-01-03 21:24:52 +00:00
jeff 06954caaaf Successful (but very manual) grpc-web requests. 2021-01-03 20:42:54 +00:00
jeff c99a41bcbd Run npm commands for building in build.rs 2021-01-03 14:07:11 +00:00
jeff 41a8514c00 Move typescript stuff to static/scripts 2021-01-03 13:46:40 +00:00
jeff ab274de581 Move templates into src 2021-01-03 13:37:55 +00:00
jeff 96baba6a50 Serve templated templates with webpack-compiled JS injected. 2021-01-03 13:36:53 +00:00
jeff e384370e24 Add some working typescript code that imports manually compiled protos. 2021-01-02 23:11:09 +00:00
jeff e601ae0149 Experimenting with tonic 2021-01-02 22:32:17 +00:00
51 changed files with 12279 additions and 491 deletions

4
.gitignore vendored
View File

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

344
Cargo.lock generated
View File

@ -98,6 +98,27 @@ 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"
@ -156,6 +177,12 @@ 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"
@ -346,7 +373,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", "base64 0.13.0",
"hkdf", "hkdf",
"percent-encoding", "percent-encoding",
"rand 0.7.3", "rand 0.7.3",
@ -1668,6 +1695,7 @@ 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]]
@ -1738,6 +1766,15 @@ 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"
@ -1835,6 +1872,21 @@ 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"
@ -1910,6 +1962,7 @@ 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",
@ -1936,7 +1989,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", "base64 0.13.0",
"blake2b_simd", "blake2b_simd",
"constant_time_eq", "constant_time_eq",
"crossbeam-utils", "crossbeam-utils",
@ -1951,6 +2004,19 @@ 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"
@ -1982,6 +2048,16 @@ 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"
@ -2150,6 +2226,12 @@ 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"
@ -2403,11 +2485,13 @@ 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]]
@ -2569,6 +2653,18 @@ 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"
@ -2592,12 +2688,226 @@ 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"
@ -2607,9 +2917,21 @@ 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"
@ -2761,6 +3083,12 @@ 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"
@ -2882,6 +3210,16 @@ 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,6 +6,9 @@ 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"
@ -16,11 +19,12 @@ 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 = "1.0" serde = { version = "1.0", features = ["derive"] }
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"
@ -28,6 +32,7 @@ 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"]}
@ -36,7 +41,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"] features = ["secrets", "tls"]
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"

View File

@ -4,41 +4,66 @@ 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.
### Install Dependencies
Install dependencies. The exact method depends on your OS.
* sqlite3 and development headers (Void Linux: `xbps-install
sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`).
* protoc: protocol buffers compiler. There is one baked into the
build, so you should not need this unless you are not using
Linux/Mac/Windows.
### Initial Setup ### Initial Setup
Follow these instructions from the root of the repository. Quick initial setup instructions that will set up a development
environment.
Set up database: 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 --version=0.2.0 sqlx-cli
cargo install cargo-run-script
cargo run --bin tenebrous-migrate cargo run --bin tenebrous-migrate
``` ```
### Run Application ### Dependencies Required
If you are using `rustup`, then it should automatically switch to the Dependencies required to build the project. The exact installation
stable version of Rust in this repository. This is because of the method depends on your OS.
`rust-toolchain` file.
* sqlite3 and development headers (Void Linux: `xbps-install
sqlite sqlite-devel`, Ubuntu: `apt install sqlite3 libsqlite3-dev`).
* protoc: protocol buffers compiler. Needed to compile protobuf files
for both the server and web frontends (Void Linux: `xbps-install
protobuf`).
* Node and npm: Needed to run webpack for compiling and injecting
Typescript into various web pages (Void Linux: `xbps-install
nodejs`).
* Docker: Needed to run the grpc proxy (Void Linux `xbps-install
docker`).
### Run Application
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
``` ```
@ -65,4 +90,15 @@ 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,3 +1,6 @@
[default]
template_dir = "generated/templates/"
[development] [development]
address = "localhost" address = "localhost"
port = 8000 port = 8000

View File

@ -1,10 +1,50 @@
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(Serialize)]"); config.type_attribute(".", "#[derive(::serde::Serialize)]");
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
config
.compile_protos( tonic_build::configure()
.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/"],
) )

54
envoy.yaml Normal file
View File

@ -0,0 +1,54 @@
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

3
envoy/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
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 Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

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

View File

@ -3,77 +3,70 @@ 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 BasicInfo { message UpdateBasicInfoRequest {
string name = 1; CharacterIdentifier character = 1;
string gender = 2; reserved 2;
string concept = 3; // string owner = 1;
string chronicle = 4; // int32 character_id = 2;
int32 age = 5;
string name = 3;
string gender = 4;
string concept = 5;
string chronicle = 6;
int32 age = 7;
} }
//Update all attributes in a Chronicles of Darkness character (or //Generic "did something succeed or not" response.
//derivative system) character sheet. This is a straight overwrite of message ApiResult {
//all basic information on the sheet. bool success = 1;
message Attributes { string error = 2;
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 {
string character_username = 1; CharacterIdentifier character = 1;
int32 character_id = 2; string attribute_name = 2;
string attribute_name = 3; int32 attribute_value = 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 SkillUpdate { message UpdateSkillRequest {
string name = 1; CharacterIdentifier character = 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 {
string character_username = 1; CharacterIdentifier character = 1;
int32 character_id = 2; string skill_name = 2;
string skill_name = 3; int32 skill_value = 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 SkillSpecializationsUpdate { message UpdateSkillSpecializationsRequest {
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 Condition { message AddConditionRequest {
string name = 1; string character_username = 1;
int32 character_id = 2;
string condition_name = 3;
} }
service CofdApi { //Remove a Condition from a Chronicles of Darkness character sheet.
rpc UpdateSkillValue(UpdateSkillValueRequest) returns (CofdSheet.Skill); message RemoveConditionRequest {
string character_username = 1;
int32 character_id = 2;
string condition_name = 3;
} }

View File

@ -39,6 +39,8 @@ 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<()>;
} }
@ -124,6 +126,25 @@ 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,6 +4,7 @@ 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)]
@ -48,6 +49,28 @@ 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]

266
src/frontend/_proto/cofd_api_pb.d.ts vendored Normal file
View File

@ -0,0 +1,266 @@
// 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

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

View File

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

373
src/frontend/_proto/cofd_pb.d.ts vendored Normal file
View File

@ -0,0 +1,373 @@
// 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

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

View File

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

View File

@ -0,0 +1,34 @@
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

@ -0,0 +1,102 @@
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

@ -84,13 +84,39 @@
} }
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script> {# Webpack Templating for API script #}
<script defer type="text/javascript" src="/scripts/api.js"></script> <%= htmlWebpackPlugin.tags.bodyTags %>
<script defer type="text/javascript" src="/scripts/characters/edit.js"></script>
<h1>Core Sheet</h1> <h1>Core Sheet</h1>
<div> <div>
<h1>Name: <input type="text" value="{{name}}" /></h1> <div id="basicInfo">
<p>System: {{data_type}}</p> <h1>
<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,8 +1,6 @@
{% 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,148 @@
{% 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

@ -0,0 +1,13 @@
{% 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

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

View File

@ -4,14 +4,11 @@ extern crate rocket;
#[macro_use] #[macro_use]
extern crate rocket_contrib; extern crate rocket_contrib;
// Seemingly necessary to get serde::Serialize into scope for Prost use log::{error, info};
// 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;
@ -20,6 +17,40 @@ 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();
@ -33,34 +64,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
migrator::migrate(db_path).await?; migrator::migrate(db_path).await?;
let root_routes: Vec<rocket::Route> = { let db = crate::db::create_pool(db_path).await?;
routes::root::routes()
.into_iter()
.chain(routes::auth::routes().into_iter())
.collect()
};
let api_routes = routes::api::routes(); make_rocket(db.clone()).await
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_derive::Serialize; use serde::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. Consumes the data. /// 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,6 +1,7 @@
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,25 +1,21 @@
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::ops::Deref; use std::io::Cursor;
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>(T) pub(crate) struct Proto<T>(pub T)
where where
T: prost::Message + Default; T: prost::Message + Default;
@ -40,21 +36,10 @@ 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())),
@ -67,6 +52,22 @@ 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
@ -78,3 +79,12 @@ 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,11 +6,50 @@ 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_derive::Serialize; use serde::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,19 +1,15 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility}; use crate::models::characters::Character;
use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use rocket_contrib::templates::Template;
use serde::Serialize; mod cofd;
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_attributes, cofd::update_attribute_value,
cofd::update_attribute, cofd::update_skill,
cofd::update_skills,
cofd::update_skill_value, cofd::update_skill_value,
cofd::add_condition, cofd::add_condition,
cofd::remove_condition cofd::remove_condition
@ -43,169 +39,3 @@ 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"
}
}

174
src/routes/api/cofd.rs Normal file
View File

@ -0,0 +1,174 @@
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,6 +3,7 @@ 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,6 +7,7 @@ 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_derive::Serialize; use serde::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]

View File

@ -1,68 +0,0 @@
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

@ -1,48 +0,0 @@
(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

@ -1,17 +0,0 @@
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

@ -1,15 +0,0 @@
{% 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 %}

17
tsconfig.json Normal file
View File

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

67
webpack.config.js Normal file
View File

@ -0,0 +1,67 @@
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'),
]
};