Initial Commit
This commit is contained in:
commit
c09b00ff35
30 changed files with 1930 additions and 0 deletions
16
.env.example
Normal file
16
.env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
DATABASE_URL=postgresql://user:pass@127.0.0.1:5432/db
|
||||
|
||||
ROCKET_ADDRESS=0.0.0.0
|
||||
ROCKET_PORT=8000
|
||||
|
||||
BOSS_CDN_URL=https://boss.example.com
|
||||
|
||||
AES_KEY=KEY
|
||||
HMAC_KEY=KEY
|
||||
|
||||
# Can be paired with an admin auth server for extra security,
|
||||
# if disabled auth will instead check to see if the sent token
|
||||
# matches ADMIN_AUTH_TOKEN
|
||||
USE_ADMIN_AUTH=true
|
||||
ADMIN_AUTH_URL=http://admin.example.com # Only needed when USE_ADMIN_AUTH=true
|
||||
ADMIN_AUTH_TOKEN=AUTH_TOKEN
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.env
|
||||
43
.gitlab-ci.yml
Normal file
43
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
default:
|
||||
image: quay.io/podman/stable
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- retag
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: rust:alpine3.22
|
||||
script:
|
||||
- cargo test
|
||||
|
||||
build-and-push-image:
|
||||
stage: build
|
||||
script:
|
||||
- apk add --no-cache musl-dev openssl-dev openssl-libs-static protobuf-dev lld
|
||||
- podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- podman build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
|
||||
- podman push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
push-retagged-branch:
|
||||
stage: retag
|
||||
script:
|
||||
- podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- podman pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
|
||||
- podman tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
|
||||
- podman push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "push"
|
||||
when: on_success
|
||||
|
||||
push-retagged-latest:
|
||||
stage: retag
|
||||
script:
|
||||
- podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- podman pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
|
||||
- podman tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" "$CI_REGISTRY_IMAGE:latest"
|
||||
- podman push "$CI_REGISTRY_IMAGE:latest"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"
|
||||
when: on_success
|
||||
15
.sqlx/query-08d43b2c58526766fe48ad002daa78fa04f3a3ef3895d0f72ae9469619a31178.json
generated
Normal file
15
.sqlx/query-08d43b2c58526766fe48ad002daa78fa04f3a3ef3895d0f72ae9469619a31178.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO files (key, data)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n key = EXCLUDED.key,\n data = EXCLUDED.data;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08d43b2c58526766fe48ad002daa78fa04f3a3ef3895d0f72ae9469619a31178"
|
||||
}
|
||||
130
.sqlx/query-3cc1241465c9bb03d570502f8ef4d8492b868f5ea0936d5e820057d7e3d3db06.json
generated
Normal file
130
.sqlx/query-3cc1241465c9bb03d570502f8ef4d8492b868f5ea0936d5e820057d7e3d3db06.json
generated
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n deleted,\n file_key,\n data_id,\n task_id,\n boss_app_id,\n supported_countries,\n supported_languages,\n attributes AS \"attributes!: Json<FileWUPAttributes>\",\n creator_user,\n name,\n type,\n hash,\n size,\n notify_on_new,\n notify_led,\n condition_played,\n auto_delete,\n created,\n updated\n FROM files_wup WHERE data_id = $1 AND deleted = false;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "file_key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "data_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "task_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "boss_app_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "supported_countries",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "supported_languages",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "attributes!: Json<FileWUPAttributes>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "creator_user",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "hash",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "size",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "notify_on_new",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "notify_led",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "condition_played",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "auto_delete",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "created",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamp"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3cc1241465c9bb03d570502f8ef4d8492b868f5ea0936d5e820057d7e3d3db06"
|
||||
}
|
||||
28
.sqlx/query-42f96f5890db3be1b4fafd30cfc3fff9163933840cc9c5cbb5dd2b4ac933473d.json
generated
Normal file
28
.sqlx/query-42f96f5890db3be1b4fafd30cfc3fff9163933840cc9c5cbb5dd2b4ac933473d.json
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO files_wup (\n file_key,\n task_id,\n boss_app_id,\n supported_countries,\n supported_languages,\n attributes,\n creator_user,\n name,\n type,\n hash,\n size,\n notify_on_new,\n notify_led,\n condition_played,\n auto_delete,\n created,\n updated\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,\n NOW() AT TIME ZONE 'UTC',\n NOW() AT TIME ZONE 'UTC'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"TextArray",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42f96f5890db3be1b4fafd30cfc3fff9163933840cc9c5cbb5dd2b4ac933473d"
|
||||
}
|
||||
80
.sqlx/query-6c4c21b1d07887b668f00a24a00b743d4527f788aa48354c5b20db582b657f75.json
generated
Normal file
80
.sqlx/query-6c4c21b1d07887b668f00a24a00b743d4527f788aa48354c5b20db582b657f75.json
generated
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM tasks WHERE deleted = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "in_game_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "boss_app_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "creator_user",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "interval",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "title_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "created",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamp"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6c4c21b1d07887b668f00a24a00b743d4527f788aa48354c5b20db582b657f75"
|
||||
}
|
||||
83
.sqlx/query-6d1d798566ce48b0931a283d858f5e72dd49100ff5b6cc86b8bd71513f718c6a.json
generated
Normal file
83
.sqlx/query-6d1d798566ce48b0931a283d858f5e72dd49100ff5b6cc86b8bd71513f718c6a.json
generated
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM tasks WHERE deleted = false AND id = $1 AND boss_app_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "in_game_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "boss_app_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "creator_user",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "interval",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "title_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "created",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamp"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6d1d798566ce48b0931a283d858f5e72dd49100ff5b6cc86b8bd71513f718c6a"
|
||||
}
|
||||
16
.sqlx/query-84520448fbf405540d5ea250c97ff57963a3164b200c42955f1d787de8f62161.json
generated
Normal file
16
.sqlx/query-84520448fbf405540d5ea250c97ff57963a3164b200c42955f1d787de8f62161.json
generated
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE files_wup\n SET deleted = TRUE,\n updated = NOW() AT TIME ZONE 'UTC'\n WHERE boss_app_id = $1 AND task_id = $2 AND name = $3;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "84520448fbf405540d5ea250c97ff57963a3164b200c42955f1d787de8f62161"
|
||||
}
|
||||
80
.sqlx/query-962610002ca8baab7b5d5e3e722dba23d4822aff9c9c3e528985631027b19c55.json
generated
Normal file
80
.sqlx/query-962610002ca8baab7b5d5e3e722dba23d4822aff9c9c3e528985631027b19c55.json
generated
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM tasks",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "in_game_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "boss_app_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "creator_user",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "interval",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "title_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "created",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamp"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "962610002ca8baab7b5d5e3e722dba23d4822aff9c9c3e528985631027b19c55"
|
||||
}
|
||||
22
.sqlx/query-c059d43146d4dc58ca32bdebd55b7734a3f4a3c3cac5a1525a84e8121b6a9c9c.json
generated
Normal file
22
.sqlx/query-c059d43146d4dc58ca32bdebd55b7734a3f4a3c3cac5a1525a84e8121b6a9c9c.json
generated
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT data FROM files WHERE key = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "data",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c059d43146d4dc58ca32bdebd55b7734a3f4a3c3cac5a1525a84e8121b6a9c9c"
|
||||
}
|
||||
21
.sqlx/query-c5e09123f159bc164e59e1aeb02165afce0ab7dafb9026ed320bc0964500c89d.json
generated
Normal file
21
.sqlx/query-c5e09123f159bc164e59e1aeb02165afce0ab7dafb9026ed320bc0964500c89d.json
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO tasks\n (id, in_game_id, boss_app_id, creator_user, status, interval, title_id, description, created, updated)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW() AT TIME ZONE 'UTC', NOW() AT TIME ZONE 'UTC')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c5e09123f159bc164e59e1aeb02165afce0ab7dafb9026ed320bc0964500c89d"
|
||||
}
|
||||
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "rboss"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
rocket_cors = "0.6.0"
|
||||
dotenvy = "0.15.7"
|
||||
sqlx = { version = "0.8.3", features = [ "postgres", "runtime-tokio", "macros", "chrono" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = "1"
|
||||
quick-xml = { version = "0.31", features = [ "serialize" ] }
|
||||
chrono = "0.4"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
hex-literal = "0.4"
|
||||
aes = "0.8"
|
||||
ctr = "0.9"
|
||||
hmac = "0.12"
|
||||
md-5 = "0.10"
|
||||
rand = "0.8"
|
||||
anyhow = "1.0"
|
||||
base64 = "0.21"
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM rust:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static protobuf-dev lld
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo fetch
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV SQLX_OFFLINE=true
|
||||
|
||||
RUN OPENSSL_LIB_DIR=/usr/lib OPENSSL_INCLUDE_DIR=/usr/include/openssl OPENSSL_STATIC=1 RUSTFLAGS="-C target-feature=+aes,+sse -C relocation-model=static -C linker=ld.lld" cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
FROM scratch AS final
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/rboss /rboss
|
||||
|
||||
ENTRYPOINT ["/rboss"]
|
||||
57
db-setup.sql
Normal file
57
db-setup.sql
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
CREATE TABLE files (
|
||||
key TEXT PRIMARY KEY,
|
||||
data BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
in_game_id TEXT NOT NULL,
|
||||
boss_app_id TEXT NOT NULL,
|
||||
creator_user TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
interval INTEGER NOT NULL,
|
||||
title_id TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created TIMESTAMP NOT NULL,
|
||||
updated TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE files_wup_data_id_seq START WITH 50000 INCREMENT BY 1;
|
||||
|
||||
CREATE TABLE files_wup (
|
||||
data_id BIGINT PRIMARY KEY DEFAULT nextval('files_wup_data_id_seq'),
|
||||
file_key TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
boss_app_id TEXT NOT NULL,
|
||||
supported_countries TEXT[] NOT NULL,
|
||||
supported_languages TEXT[] NOT NULL,
|
||||
attributes JSONB NOT NULL,
|
||||
creator_user TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
size BIGINT NOT NULL,
|
||||
notify_on_new TEXT[] NOT NULL,
|
||||
notify_led BOOLEAN NOT NULL,
|
||||
condition_played BIGINT NOT NULL,
|
||||
auto_delete BOOLEAN NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created TIMESTAMP NOT NULL,
|
||||
updated TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION files_wup_set_name_from_data_id()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.name IS NULL OR NEW.name = '' THEN
|
||||
NEW.name := NEW.data_id::text;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER files_wup_set_name
|
||||
BEFORE INSERT ON files_wup
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION files_wup_set_name_from_data_id();
|
||||
71
src/admin_auth.rs
Normal file
71
src/admin_auth.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use std::env;
|
||||
use rocket::{Request, http::Status, request::{FromRequest, Outcome}};
|
||||
use reqwest::Client;
|
||||
|
||||
pub struct AdminAuth(pub String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminAuth {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let use_admin_server = env::var("USE_ADMIN_AUTH")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
if use_admin_server {
|
||||
let auth = match req.headers().get_one("Authorization") {
|
||||
Some(auth) => auth,
|
||||
None => return Outcome::Error((Status::BadRequest, ())),
|
||||
};
|
||||
|
||||
let auth_url = match env::var("ADMIN_AUTH_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
||||
};
|
||||
let auth_token = match env::var("ADMIN_AUTH_TOKEN") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
||||
};
|
||||
|
||||
let http = Client::new();
|
||||
let response = http.get(format!("{}/api/v1/validate_token/boss", auth_url))
|
||||
.header("Authorization", format!("InterServer {}", auth_token))
|
||||
.header("Token", auth)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
match response.error_for_status() {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(text) => return Outcome::Success(AdminAuth(text)),
|
||||
Err(_) => return Outcome::Error((Status::InternalServerError, ()))
|
||||
},
|
||||
Err(_) => {
|
||||
return Outcome::Error((Status::Unauthorized, ()));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
||||
}
|
||||
} else {
|
||||
let local_token = match env::var("ADMIN_AUTH_TOKEN") {
|
||||
Ok(token) => token,
|
||||
Err(_) => return Outcome::Error((Status::InternalServerError, ()))
|
||||
};
|
||||
|
||||
let token = match req.headers().get_one("Authorization") {
|
||||
Some(auth) => match auth.strip_prefix("Bearer ") {
|
||||
Some(token) => token,
|
||||
None => return Outcome::Error((Status::BadRequest, ()))
|
||||
},
|
||||
None => return Outcome::Error((Status::BadRequest, ())),
|
||||
};
|
||||
|
||||
if token != local_token.as_str() { return Outcome::Error((Status::Unauthorized, ())) }
|
||||
|
||||
Outcome::Success(AdminAuth("admin".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/api/add_task.rs
Normal file
51
src/api/add_task.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use serde::Deserialize;
|
||||
use rocket::{State, http::Status};
|
||||
use rocket::serde::json::Json;
|
||||
use crate::Pool;
|
||||
use crate::admin_auth::AdminAuth;
|
||||
use crate::models::task::is_valid_task_status;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AddTaskData {
|
||||
task_id: String,
|
||||
boss_app_id: String,
|
||||
title_id: String,
|
||||
status: String,
|
||||
interval: i32,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[rocket::post("/api/v1/add_task", data = "<input>")]
|
||||
pub async fn add_task(pool: &State<Pool>, input: Json<AddTaskData>, auth: AdminAuth) -> Result<(), Status> {
|
||||
let pool = pool.inner();
|
||||
let data = input.into_inner();
|
||||
let admin_username = auth.0;
|
||||
|
||||
if data.boss_app_id.len() != 16 { return Err(Status::BadRequest) };
|
||||
|
||||
if !is_valid_task_status(&data.status) { return Err(Status::BadRequest) };
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO tasks
|
||||
(id, in_game_id, boss_app_id, creator_user, status, interval, title_id, description, created, updated)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW() AT TIME ZONE 'UTC', NOW() AT TIME ZONE 'UTC')
|
||||
"#,
|
||||
data.task_id[0..7].to_string(),
|
||||
data.task_id,
|
||||
data.boss_app_id,
|
||||
admin_username,
|
||||
data.status,
|
||||
data.interval,
|
||||
data.title_id,
|
||||
data.description,
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::Database(_) => Status::Conflict,
|
||||
_ => Status::InternalServerError,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
2
src/api/mod.rs
Normal file
2
src/api/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod upload_file_wup;
|
||||
pub mod add_task;
|
||||
189
src/api/upload_file_wup.rs
Normal file
189
src/api/upload_file_wup.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use std::env;
|
||||
use serde::Deserialize;
|
||||
use serde_json::to_value;
|
||||
use rocket::{State, http::Status};
|
||||
use rocket::serde::json::Json;
|
||||
use md5::{Md5, Digest};
|
||||
use crate::Pool;
|
||||
use crate::admin_auth::AdminAuth;
|
||||
use crate::models::file_wup::{is_valid_countries, is_valid_file_notify_conditions, is_valid_file_type, is_valid_languages};
|
||||
use crate::{crypto::wiiu::encrypt_wiiu, models::file_wup::FileWUPAttributes};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UploadedWUP {
|
||||
task_id: String,
|
||||
boss_app_id: String,
|
||||
supported_countries: Vec<String>,
|
||||
supported_languages: Vec<String>,
|
||||
name: Option<String>,
|
||||
r#type: String,
|
||||
notify_on_new: Vec<String>,
|
||||
notify_led: bool,
|
||||
data: String,
|
||||
attributes: Option<FileWUPAttributes>,
|
||||
name_equals_data_id: bool,
|
||||
condition_played: i64,
|
||||
auto_delete: bool,
|
||||
}
|
||||
|
||||
#[rocket::post("/api/v1/upl_wup", data = "<input>")]
|
||||
pub async fn upload_file_wup(pool: &State<Pool>, input: Json<UploadedWUP>, auth: AdminAuth) -> Result<(), Status> {
|
||||
let pool = pool.inner();
|
||||
let data = input.into_inner();
|
||||
let admin_username = auth.0;
|
||||
|
||||
|
||||
|
||||
let aes_key = match env::var("AES_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let hmac_key = match env::var("HMAC_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let data_bytes = match base64::decode(data.data) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(Status::BadRequest)
|
||||
};
|
||||
|
||||
|
||||
|
||||
if data.name == None && !data.name_equals_data_id { return Err(Status::BadRequest) };
|
||||
let name = data.name.clone().unwrap();
|
||||
|
||||
if data.boss_app_id.len() != 16 { return Err(Status::BadRequest) };
|
||||
|
||||
if !is_valid_countries(&data.supported_countries) { return Err(Status::BadRequest) };
|
||||
if !is_valid_languages(&data.supported_languages) { return Err(Status::BadRequest) };
|
||||
|
||||
if !is_valid_file_type(&data.r#type) { return Err(Status::BadRequest) };
|
||||
|
||||
if !is_valid_file_notify_conditions(&data.notify_on_new) { return Err(Status::BadRequest) };
|
||||
|
||||
if data_bytes.len() == 0 { return Err(Status::BadRequest) };
|
||||
|
||||
|
||||
|
||||
let attributes = match data.attributes {
|
||||
Some(attr) => attr,
|
||||
None => FileWUPAttributes{
|
||||
attribute1: "".to_string(),
|
||||
attribute2: "".to_string(),
|
||||
attribute3: "".to_string(),
|
||||
description: "".to_string(),
|
||||
}
|
||||
};
|
||||
let attributes = match to_value(attributes) {
|
||||
Ok(attr) => attr,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let encrypted_data = match &data_bytes[0..4] == b"boss" {
|
||||
true => data_bytes,
|
||||
false => match encrypt_wiiu(&data_bytes, &aes_key.as_bytes(), &hmac_key.as_bytes()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(Status::InternalServerError)
|
||||
},
|
||||
};
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(encrypted_data.clone());
|
||||
let hash_bytes = &hasher.finalize()[..];
|
||||
let hash = hex::encode(hash_bytes);
|
||||
|
||||
let file_key = format!("{}/{}/{}", data.boss_app_id, data.task_id, hash);
|
||||
|
||||
let _ = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO files (key, data)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET
|
||||
key = EXCLUDED.key,
|
||||
data = EXCLUDED.data;
|
||||
"#,
|
||||
file_key,
|
||||
encrypted_data,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
// Set old file to deleted if it previously existed
|
||||
let _ = sqlx::query!(
|
||||
r#"
|
||||
UPDATE files_wup
|
||||
SET deleted = TRUE,
|
||||
updated = NOW() AT TIME ZONE 'UTC'
|
||||
WHERE boss_app_id = $1 AND task_id = $2 AND name = $3;
|
||||
"#,
|
||||
&data.boss_app_id,
|
||||
&data.task_id,
|
||||
name
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
let file_name = match data.name.clone() {
|
||||
Some(name) => {
|
||||
if data.name_equals_data_id {
|
||||
"".to_string()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
},
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO files_wup (
|
||||
file_key,
|
||||
task_id,
|
||||
boss_app_id,
|
||||
supported_countries,
|
||||
supported_languages,
|
||||
attributes,
|
||||
creator_user,
|
||||
name,
|
||||
type,
|
||||
hash,
|
||||
size,
|
||||
notify_on_new,
|
||||
notify_led,
|
||||
condition_played,
|
||||
auto_delete,
|
||||
created,
|
||||
updated
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
|
||||
NOW() AT TIME ZONE 'UTC',
|
||||
NOW() AT TIME ZONE 'UTC'
|
||||
)
|
||||
"#,
|
||||
file_key,
|
||||
data.task_id,
|
||||
data.boss_app_id,
|
||||
&data.supported_countries,
|
||||
&data.supported_languages,
|
||||
attributes,
|
||||
admin_username,
|
||||
file_name,
|
||||
data.r#type,
|
||||
hash,
|
||||
encrypted_data.len() as i64,
|
||||
&data.notify_on_new,
|
||||
data.notify_led,
|
||||
data.condition_played,
|
||||
data.auto_delete,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(Status::InternalServerError)
|
||||
}
|
||||
}
|
||||
1
src/crypto/mod.rs
Normal file
1
src/crypto/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod wiiu;
|
||||
105
src/crypto/wiiu.rs
Normal file
105
src/crypto/wiiu.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use aes::Aes128;
|
||||
use ctr::cipher::{KeyIvInit, StreamCipher};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use md5::{Md5, Digest};
|
||||
use rand::RngCore;
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
type Aes128Ctr = ctr::Ctr128BE<Aes128>;
|
||||
|
||||
const BOSS_WUP_VER: u32 = 0x20001;
|
||||
|
||||
const BOSS_AES_KEY_HASH: [u8; 16] = hex_literal::hex!("5202ce5099232c3d365e28379790a919");
|
||||
const BOSS_HMAC_KEY_HASH: [u8; 16] = hex_literal::hex!("b4482fef177b0100090ce0dbeb8ce977");
|
||||
|
||||
pub struct WupBossInfo {
|
||||
pub hash_type: u16,
|
||||
pub iv: Vec<u8>,
|
||||
pub hmac: Vec<u8>,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
fn verify_keys(aes_key: &[u8], hmac_key: &[u8]) -> Result<()> {
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(aes_key);
|
||||
let aes_md5 = hasher.finalize_reset();
|
||||
|
||||
if aes_md5[..] != BOSS_AES_KEY_HASH {
|
||||
bail!("Invalid BOSS AES key");
|
||||
}
|
||||
|
||||
hasher.update(hmac_key);
|
||||
let hmac_md5 = hasher.finalize();
|
||||
|
||||
if hmac_md5[..] != BOSS_HMAC_KEY_HASH {
|
||||
bail!("Invalid BOSS HMAC key");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_wiiu(data: &[u8], aes_key: &[u8], hmac_key: &[u8]) -> Result<WupBossInfo> {
|
||||
verify_keys(aes_key, hmac_key)?;
|
||||
|
||||
let hash_type = u16::from_be_bytes([data[0xA], data[0xB]]);
|
||||
if hash_type != 2 {
|
||||
bail!("Unknown hash type");
|
||||
}
|
||||
|
||||
let mut iv = Vec::with_capacity(16);
|
||||
iv.extend_from_slice(&data[0xC..0x18]);
|
||||
iv.extend_from_slice(&[0, 0, 0, 1]);
|
||||
|
||||
let mut cipher = Aes128Ctr::new_from_slices(aes_key, &iv)?;
|
||||
let mut decrypted = data[0x20..].to_vec();
|
||||
cipher.apply_keystream(&mut decrypted);
|
||||
|
||||
let hmac = decrypted[..0x20].to_vec();
|
||||
let content = decrypted[0x20..].to_vec();
|
||||
|
||||
let mut mac = HmacSha256::new_from_slice(hmac_key)?;
|
||||
mac.update(&content);
|
||||
mac.verify_slice(&hmac)
|
||||
.map_err(|_| anyhow::anyhow!("Content HMAC check failed"))?;
|
||||
|
||||
Ok(WupBossInfo {
|
||||
hash_type,
|
||||
iv,
|
||||
hmac,
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_wiiu(content: &[u8], aes_key: &[u8], hmac_key: &[u8]) -> Result<Vec<u8>> {
|
||||
verify_keys(aes_key, hmac_key)?;
|
||||
|
||||
let mut mac = HmacSha256::new_from_slice(hmac_key)?;
|
||||
mac.update(content);
|
||||
let hmac = mac.finalize().into_bytes();
|
||||
|
||||
let mut plaintext = Vec::new();
|
||||
plaintext.extend_from_slice(&hmac);
|
||||
plaintext.extend_from_slice(content);
|
||||
|
||||
let mut iv12 = [0u8; 12];
|
||||
rand::thread_rng().fill_bytes(&mut iv12);
|
||||
|
||||
let mut iv = Vec::with_capacity(16);
|
||||
iv.extend_from_slice(&iv12);
|
||||
iv.extend_from_slice(&[0, 0, 0, 1]);
|
||||
|
||||
let mut cipher = Aes128Ctr::new_from_slices(aes_key, &iv)?;
|
||||
cipher.apply_keystream(&mut plaintext);
|
||||
|
||||
let mut header = vec![0u8; 0x20];
|
||||
header[0..4].copy_from_slice(b"boss");
|
||||
header[0x4..0x8].copy_from_slice(&BOSS_WUP_VER.to_be_bytes());
|
||||
header[0x8..0xA].copy_from_slice(&1u16.to_be_bytes());
|
||||
header[0xA..0xC].copy_from_slice(&2u16.to_be_bytes());
|
||||
header[0xC..0x18].copy_from_slice(&iv12);
|
||||
|
||||
header.extend_from_slice(&plaintext);
|
||||
Ok(header)
|
||||
}
|
||||
169
src/database.rs
Normal file
169
src/database.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use crate::Pool;
|
||||
use crate::models::task::Task;
|
||||
use crate::models::file_wup::FileWUP;
|
||||
use crate::models::file_wup::FileWUPAttributes;
|
||||
use sqlx::QueryBuilder;
|
||||
use sqlx::types::Json;
|
||||
|
||||
pub async fn get_all_tasks(pool: &Pool, allow_deleted: bool) -> Vec<Task> {
|
||||
if allow_deleted {
|
||||
sqlx::query_as!(
|
||||
Task,
|
||||
"SELECT * FROM tasks",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.expect("couldn't get all tasks")
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
Task,
|
||||
"SELECT * FROM tasks WHERE deleted = false",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.expect("couldn't get all tasks")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_task(pool: &Pool, boss_app_id: String, task_id: String) -> Option<Task> {
|
||||
sqlx::query_as!(
|
||||
Task,
|
||||
"SELECT * FROM tasks WHERE deleted = false AND id = $1 AND boss_app_id = $2",
|
||||
&task_id[0..7],
|
||||
boss_app_id,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.expect("couldnt get task")
|
||||
}
|
||||
|
||||
pub async fn get_wup_task_files(
|
||||
pool: &Pool,
|
||||
allow_deleted: bool,
|
||||
boss_app_id: String,
|
||||
task_id: String,
|
||||
country:Option<String>,
|
||||
language: Option<String>,
|
||||
any: Option<bool>
|
||||
) -> Vec<FileWUP> {
|
||||
let any = any.unwrap_or(false);
|
||||
|
||||
let mut qb = QueryBuilder::new(
|
||||
"SELECT * FROM files_wup WHERE 1=1"
|
||||
);
|
||||
|
||||
if !allow_deleted { qb.push(" AND deleted = false"); };
|
||||
|
||||
qb.push(" AND boss_app_id = ");
|
||||
qb.push_bind(boss_app_id);
|
||||
|
||||
qb.push(" AND task_id = ");
|
||||
qb.push_bind(task_id);
|
||||
|
||||
if let Some(country) = country {
|
||||
qb.push(" AND (supported_countries = '{}' OR supported_countries @> ARRAY[");
|
||||
qb.push_bind(country);
|
||||
qb.push("])");
|
||||
} else if !any {
|
||||
qb.push(" AND supported_countries = '{}'");
|
||||
}
|
||||
|
||||
|
||||
if let Some(language) = language {
|
||||
qb.push(" AND (supported_languages = '{}' OR supported_languages @> ARRAY[");
|
||||
qb.push_bind(language);
|
||||
qb.push("])");
|
||||
} else if !any {
|
||||
qb.push(" AND supported_languages = '{}'");
|
||||
}
|
||||
|
||||
|
||||
let response = qb.build_query_as::<FileWUP>().fetch_all(pool).await;
|
||||
|
||||
match response {
|
||||
Ok(response) => return response,
|
||||
Err(_) => return vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_wup_task_file(
|
||||
pool: &Pool,
|
||||
allow_deleted: bool,
|
||||
boss_app_id: String,
|
||||
task_id: String,
|
||||
name: String,
|
||||
country: Option<String>,
|
||||
language: Option<String>,
|
||||
any: Option<bool>
|
||||
) -> Option<FileWUP> {
|
||||
let any = any.unwrap_or(false);
|
||||
|
||||
let mut qb = QueryBuilder::new(
|
||||
"SELECT * FROM files_wup WHERE name = "
|
||||
);
|
||||
qb.push_bind(name);
|
||||
|
||||
if !allow_deleted { qb.push(" AND deleted = false"); };
|
||||
|
||||
qb.push(" AND boss_app_id = ");
|
||||
qb.push_bind(boss_app_id);
|
||||
|
||||
qb.push(" AND task_id = ");
|
||||
qb.push_bind(&task_id[..7]);
|
||||
|
||||
if let Some(country) = country {
|
||||
qb.push(" AND (supported_countries = '{}' OR supported_countries @> ARRAY[");
|
||||
qb.push_bind(country);
|
||||
qb.push("])");
|
||||
} else if !any {
|
||||
qb.push(" AND supported_countries = '{}'");
|
||||
}
|
||||
|
||||
|
||||
if let Some(language) = language {
|
||||
qb.push(" AND (supported_languages = '{}' OR supported_languages @> ARRAY[");
|
||||
qb.push_bind(language);
|
||||
qb.push("])");
|
||||
} else if !any {
|
||||
qb.push(" AND supported_languages = '{}'");
|
||||
}
|
||||
|
||||
|
||||
let response = qb.build_query_as::<FileWUP>().fetch_optional(pool).await;
|
||||
|
||||
match response {
|
||||
Ok(response) => return response,
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_wup_task_file_by_data_id(pool: &Pool, data_id: i64) -> Option<FileWUP> {
|
||||
sqlx::query_as!(
|
||||
FileWUP,
|
||||
r#"SELECT
|
||||
deleted,
|
||||
file_key,
|
||||
data_id,
|
||||
task_id,
|
||||
boss_app_id,
|
||||
supported_countries,
|
||||
supported_languages,
|
||||
attributes AS "attributes!: Json<FileWUPAttributes>",
|
||||
creator_user,
|
||||
name,
|
||||
type,
|
||||
hash,
|
||||
size,
|
||||
notify_on_new,
|
||||
notify_led,
|
||||
condition_played,
|
||||
auto_delete,
|
||||
created,
|
||||
updated
|
||||
FROM files_wup WHERE data_id = $1 AND deleted = false;"#,
|
||||
data_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.expect("couldnt find wup task file by data id")
|
||||
}
|
||||
73
src/main.rs
Normal file
73
src/main.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use std::env;
|
||||
use dotenvy::dotenv;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use rocket::{routes, catchers, Request};
|
||||
use rocket::http::{Method, ContentType, Status};
|
||||
use rocket::response::content::RawXml;
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
|
||||
mod models;
|
||||
mod services;
|
||||
mod database;
|
||||
mod api;
|
||||
mod crypto;
|
||||
mod admin_auth;
|
||||
|
||||
type Pool = sqlx::Pool<sqlx::Postgres>;
|
||||
|
||||
#[rocket::catch(404)]
|
||||
fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) {
|
||||
(
|
||||
Status::NotFound,
|
||||
(
|
||||
ContentType::XML,
|
||||
RawXml(
|
||||
r#"<?xml version="1.0"?>
|
||||
<errors>
|
||||
<error>
|
||||
<cause/>
|
||||
<code>0008</code>
|
||||
<message>Not found</message>
|
||||
</error>
|
||||
</errors>"#,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[rocket::launch]
|
||||
async fn launch() -> _ {
|
||||
dotenv().ok();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("No database URL set");
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&database_url).await
|
||||
.expect("Unable to create database pool");
|
||||
|
||||
let cors = CorsOptions::default()
|
||||
.allowed_origins(AllowedOrigins::All)
|
||||
.allowed_methods(
|
||||
vec![Method::Get]
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect(),
|
||||
)
|
||||
.allow_credentials(true);
|
||||
|
||||
rocket::build()
|
||||
.attach(cors.to_cors().unwrap())
|
||||
.manage(pool)
|
||||
.mount("/", routes![
|
||||
services::nppl::policylist,
|
||||
services::nppl::policylist_consoletype,
|
||||
services::npts::tasksheet,
|
||||
services::npts::tasksheet_no_boss_app_id,
|
||||
services::npts::tasksheet_file,
|
||||
services::npdi::data,
|
||||
api::upload_file_wup::upload_file_wup,
|
||||
api::add_task::add_task,
|
||||
])
|
||||
.register("/", catchers![not_found])
|
||||
}
|
||||
95
src/models/file_wup.rs
Normal file
95
src/models/file_wup.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::{FromRow, Decode, Encode, types::Json};
|
||||
|
||||
const VALID_COUNTRIES: &[&str] = &[
|
||||
"GB", "US", "IT", "NL", "DE", "CA", "FR", "HU", "CR",
|
||||
"AU", "BR", "RO", "CL", "MX", "RU", "ES", "JP", "CZ",
|
||||
"PT", "MT", "AR", "SE", "PL", "IE", "BE", "HT", "NO",
|
||||
"FI", "GR", "BO", "AT", "VE", "PA", "PE", "GF", "SA",
|
||||
"CO", "LT", "NA", "CH", "CY", "RS", "KY", "GP", "DK",
|
||||
"KR", "LU", "SV", "VA", "GT", "SK", "HR", "ZA", "DO",
|
||||
"UY", "LV", "HN", "JM", "TR", "IN", "ER", "AW", "NZ",
|
||||
"EC", "TW", "EE", "CN", "SI", "AI", "BG", "NI", "IS",
|
||||
"MQ", "BZ", "BA", "MY", "AZ", "ZW", "AL", "IM", "VG",
|
||||
"VI", "BM", "GY", "SR", "MS", "TC", "BB", "TT",
|
||||
];
|
||||
|
||||
const VALID_LANGUAGES: &[&str] = &[
|
||||
"en", "it", "de", "fr", "es", "us", "pt", "ru", "ja", "nl", "ko", "zh", "tw",
|
||||
];
|
||||
|
||||
const VALID_FILE_TYPES: &[&str] = &[
|
||||
"Message",
|
||||
"AppData",
|
||||
];
|
||||
|
||||
const VALID_FILE_NOTIFY_CONDITIONS: &[&str] = &[
|
||||
"app",
|
||||
"account",
|
||||
];
|
||||
|
||||
pub fn is_valid_countries(countries: &Vec<String>) -> bool {
|
||||
for country in countries {
|
||||
match VALID_COUNTRIES.contains(&country.as_str()) {
|
||||
true => (),
|
||||
false => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_valid_languages(languages: &Vec<String>) -> bool {
|
||||
for lang in languages {
|
||||
match VALID_LANGUAGES.contains(&lang.as_str()) {
|
||||
true => (),
|
||||
false => return false,
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_valid_file_type(file_type: &str) -> bool {
|
||||
VALID_FILE_TYPES.contains(&file_type)
|
||||
}
|
||||
|
||||
pub fn is_valid_file_notify_conditions(conditions: &Vec<String>) -> bool {
|
||||
for condition in conditions {
|
||||
match VALID_FILE_NOTIFY_CONDITIONS.contains(&condition.as_str()) {
|
||||
true => (),
|
||||
false => return false,
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Decode, Encode, Clone)]
|
||||
pub struct FileWUPAttributes {
|
||||
pub attribute1: String,
|
||||
pub attribute2: String,
|
||||
pub attribute3: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone)]
|
||||
pub struct FileWUP {
|
||||
pub deleted: bool,
|
||||
pub file_key: String,
|
||||
pub data_id: Option<i64>,
|
||||
pub task_id: String,
|
||||
pub boss_app_id: String,
|
||||
pub supported_countries: Vec<String>,
|
||||
pub supported_languages: Vec<String>,
|
||||
pub attributes: Json<FileWUPAttributes>,
|
||||
pub creator_user: String,
|
||||
pub name: String,
|
||||
pub r#type: String,
|
||||
pub hash: String,
|
||||
pub size: i64,
|
||||
pub notify_on_new: Vec<String>,
|
||||
pub notify_led: bool,
|
||||
pub condition_played: i64,
|
||||
pub auto_delete: bool,
|
||||
pub created: NaiveDateTime,
|
||||
pub updated: NaiveDateTime,
|
||||
}
|
||||
2
src/models/mod.rs
Normal file
2
src/models/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod task;
|
||||
pub mod file_wup;
|
||||
27
src/models/task.rs
Normal file
27
src/models/task.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::Type;
|
||||
|
||||
const VALID_TASK_STATUSES: &[&str] = &[
|
||||
"open",
|
||||
"close",
|
||||
];
|
||||
|
||||
pub fn is_valid_task_status(status: &str) -> bool {
|
||||
VALID_TASK_STATUSES.contains(&status)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Task {
|
||||
pub deleted: bool,
|
||||
pub id: String,
|
||||
pub in_game_id: String,
|
||||
pub boss_app_id: String,
|
||||
pub creator_user: String,
|
||||
pub status: String,
|
||||
pub interval: i32,
|
||||
pub title_id: String,
|
||||
pub description: String,
|
||||
pub created: NaiveDateTime,
|
||||
pub updated: NaiveDateTime,
|
||||
}
|
||||
3
src/services/mod.rs
Normal file
3
src/services/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod nppl;
|
||||
pub mod npts;
|
||||
pub mod npdi;
|
||||
56
src/services/npdi.rs
Normal file
56
src/services/npdi.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::io::Cursor;
|
||||
use rocket::http::{Header, Status};
|
||||
use rocket::{Data, State};
|
||||
use rocket::response::{Response, Responder};
|
||||
use crate::Pool;
|
||||
use crate::database::get_wup_task_file_by_data_id;
|
||||
|
||||
#[derive(Responder)]
|
||||
pub struct DataResponder<T> {
|
||||
inner: T,
|
||||
content_type_header: Header<'static>,
|
||||
content_disposition_header: Header<'static>,
|
||||
content_transfer_encoding_header: Header<'static>,
|
||||
content_length_header: Header<'static>,
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r, T: Responder<'r, 'o>> DataResponder<T> {
|
||||
fn new(inner: T, content_length: String) -> Self {
|
||||
DataResponder {
|
||||
inner,
|
||||
content_type_header: Header::new("Content-Type", "applicatoin/octet-stream"), // Intentional spelling mistake
|
||||
content_disposition_header: Header::new("Content-Disposition", "attachment"),
|
||||
content_transfer_encoding_header: Header::new("Content-Transfer-Encoding", "binary"),
|
||||
content_length_header: Header::new("Content-Length", content_length),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::get("/p01/data/1/<boss_app_id>/<data_id>/<file_hash>")]
|
||||
pub async fn data(pool: &State<Pool>, boss_app_id: String, data_id: i64, file_hash: String) -> Result<DataResponder<Vec<u8>>, Status> {
|
||||
let pool = pool.inner();
|
||||
|
||||
let file_wup = get_wup_task_file_by_data_id(pool, data_id).await;
|
||||
|
||||
let file_wup = match file_wup {
|
||||
Some(file_wup) => file_wup,
|
||||
None => return Err(Status::NotFound),
|
||||
};
|
||||
|
||||
if file_wup.hash != file_hash || file_wup.boss_app_id != boss_app_id { return Err(Status::NotFound); }
|
||||
|
||||
let file = sqlx::query_scalar!(
|
||||
"SELECT data FROM files WHERE key = $1",
|
||||
file_wup.file_key,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
let file = match file {
|
||||
Ok(Some(file)) => { file },
|
||||
Ok(None) => return Err(Status::NotFound),
|
||||
Err(_) => return Err(Status::NotFound),
|
||||
};
|
||||
|
||||
Ok(DataResponder::new(file, file_wup.size.to_string()))
|
||||
}
|
||||
241
src/services/nppl.rs
Normal file
241
src/services/nppl.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use rocket::http::Status;
|
||||
use serde::Serialize;
|
||||
use chrono::Utc;
|
||||
|
||||
/* Commented out until database support is added
|
||||
use rocket::State;
|
||||
use crate::Pool;
|
||||
*/
|
||||
|
||||
// TODO:
|
||||
// - Use database for policy lists for easier modification if needed
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum TaskLevel {
|
||||
STOPPED,
|
||||
HIGH,
|
||||
EXPEDITE,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct PolicyPriority {
|
||||
title_id: String,
|
||||
task_id: String,
|
||||
level: TaskLevel,
|
||||
persistent: Option<bool>,
|
||||
revive: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct PolicyList {
|
||||
major_version: i32,
|
||||
minor_version: i32,
|
||||
list_id: i32,
|
||||
default_stop: bool,
|
||||
force_version_up: bool,
|
||||
update_time: String,
|
||||
priority: Vec<PolicyPriority>,
|
||||
}
|
||||
|
||||
async fn get_policy_list(console_type: String, major_version: String, country_code: String) -> Result<String, Status> {
|
||||
|
||||
let policy_list: Option<PolicyList> = match console_type.as_str() {
|
||||
|
||||
"0" => { // 3DS
|
||||
if major_version != "3" { None }
|
||||
else {
|
||||
Some(PolicyList{
|
||||
major_version: 3,
|
||||
minor_version: 0,
|
||||
list_id: 1891,
|
||||
default_stop: false,
|
||||
force_version_up: false,
|
||||
update_time: Utc::now().format("%Y-%m-%dT%H:%M:%S+0000").to_string(),
|
||||
priority: vec![
|
||||
PolicyPriority{
|
||||
title_id: "0004003000008f02".to_string(),
|
||||
task_id: "basho0".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000400300000bc00".to_string(),
|
||||
task_id: "OlvNotf".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000400300000bd00".to_string(),
|
||||
task_id: "OlvNotf".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000400300000be00".to_string(),
|
||||
task_id: "OlvNotf".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0004003000008f02".to_string(),
|
||||
task_id: "pl".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0004013000003400".to_string(),
|
||||
task_id: "sprelay".to_string(),
|
||||
level: TaskLevel::HIGH,
|
||||
persistent: Some(true),
|
||||
revive: Some(true),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
"1" => { // Wii U
|
||||
if major_version != "1" { None }
|
||||
else {
|
||||
Some(PolicyList{
|
||||
major_version: 1,
|
||||
minor_version: 0,
|
||||
list_id: 1891,
|
||||
default_stop: false,
|
||||
force_version_up: false,
|
||||
update_time: Utc::now().format("%Y-%m-%dT%H:%M:%S+0000").to_string(),
|
||||
priority: vec![
|
||||
PolicyPriority{
|
||||
title_id: "0005003010016000".to_string(),
|
||||
task_id: "olvinfo".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0005003010016100".to_string(),
|
||||
task_id: "olvinfo".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0005003010016200".to_string(),
|
||||
task_id: "olvinfo".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500301001600a".to_string(),
|
||||
task_id: "olv1".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500301001610a".to_string(),
|
||||
task_id: "olv1".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500301001620a".to_string(),
|
||||
task_id: "olv1".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0005001010040000".to_string(),
|
||||
task_id: "olvtopic".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0005001010040100".to_string(),
|
||||
task_id: "olvtopic".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "0005001010040200".to_string(),
|
||||
task_id: "olvtopic".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500101005a000".to_string(),
|
||||
task_id: "Chat".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500101005a100".to_string(),
|
||||
task_id: "Chat".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500101005a200".to_string(),
|
||||
task_id: "Chat".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
PolicyPriority{
|
||||
title_id: "000500101004c100".to_string(),
|
||||
task_id: "plog".to_string(),
|
||||
level: TaskLevel::EXPEDITE,
|
||||
persistent: None,
|
||||
revive: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
_ => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
match policy_list {
|
||||
|
||||
Some(policy_list) => {
|
||||
|
||||
let xml = quick_xml::se::to_string(&policy_list);
|
||||
match xml {
|
||||
Ok(xml) => return Ok(xml),
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
None => return Err(Status::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[rocket::get("/p01/policylist/<major_version>/<country_code>")]
|
||||
pub async fn policylist(major_version: String, country_code: String) -> Result<String, Status> {
|
||||
let console_type = "0".to_string();
|
||||
get_policy_list(console_type, major_version, country_code).await
|
||||
}
|
||||
|
||||
#[rocket::get("/p01/policylist/<console_type>/<major_version>/<country_code>")]
|
||||
pub async fn policylist_consoletype(console_type: String, major_version: String, country_code: String) -> Result<String, Status> {
|
||||
get_policy_list(console_type, major_version, country_code).await
|
||||
}
|
||||
202
src/services/npts.rs
Normal file
202
src/services/npts.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
use std::env;
|
||||
use rocket::State;
|
||||
use rocket::form::FromForm;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::content::RawXml;
|
||||
use serde::Serialize;
|
||||
use crate::Pool;
|
||||
use crate::database::{get_task, get_wup_task_files, get_wup_task_file};
|
||||
use crate::models::task::Task;
|
||||
use crate::models::file_wup::FileWUP;
|
||||
use crate::models::file_wup::FileWUPAttributes;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct QueryParams {
|
||||
c: Option<String>,
|
||||
l: Option<String>,
|
||||
mode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct FileWUPNotify {
|
||||
new: String,
|
||||
led: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct FileWUPFormatted {
|
||||
filename: String,
|
||||
data_id: Option<i64>,
|
||||
r#type: String,
|
||||
url: Option<String>,
|
||||
size: i64,
|
||||
attributes: Option<FileWUPAttributes>,
|
||||
notify: Option<FileWUPNotify>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct TaskSheetFiles {
|
||||
file: Vec<FileWUPFormatted>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct TaskSheet {
|
||||
title_id: String,
|
||||
task_id: String,
|
||||
service_status: String,
|
||||
files: TaskSheetFiles,
|
||||
}
|
||||
|
||||
fn build_file(task: Task, file: &FileWUP, attributes_mode: bool, cdn_url: &String) -> FileWUPFormatted {
|
||||
let file = file.clone();
|
||||
if attributes_mode {
|
||||
FileWUPFormatted {
|
||||
filename: file.name,
|
||||
r#type: file.r#type,
|
||||
size: file.size,
|
||||
attributes: Some(FileWUPAttributes {
|
||||
attribute1: file.attributes.attribute1.clone(),
|
||||
attribute2: file.attributes.attribute2.clone(),
|
||||
attribute3: file.attributes.attribute3.clone(),
|
||||
description: file.attributes.description.clone(),
|
||||
}),
|
||||
data_id: None,
|
||||
url: None,
|
||||
notify: None,
|
||||
}
|
||||
} else {
|
||||
let url = format!("{}/p01/data/1/{}/{}/{}", cdn_url, task.boss_app_id, file.data_id.unwrap(), file.hash);
|
||||
FileWUPFormatted {
|
||||
filename: file.name,
|
||||
data_id: Some(file.data_id.unwrap()),
|
||||
r#type: file.r#type,
|
||||
url: Some(url),
|
||||
size: file.size,
|
||||
notify: Some(FileWUPNotify {
|
||||
new: file.notify_on_new.join(","),
|
||||
led: file.notify_led,
|
||||
}),
|
||||
attributes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::get("/p01/tasksheet/<id>/<boss_app_id>/<task_id>?<query..>")]
|
||||
pub async fn tasksheet(pool: &State<Pool>, id: &str, boss_app_id: &str, task_id: &str, query: QueryParams) -> Result<RawXml<String>, Status> {
|
||||
let pool = pool.inner();
|
||||
let cdn_url = match env::var("BOSS_CDN_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let country = query.c;
|
||||
let language = query.l;
|
||||
let mode = query.mode;
|
||||
|
||||
let task = match get_task(pool, boss_app_id.to_string(), task_id.to_string()).await {
|
||||
Some(task) => task,
|
||||
None => return Err(Status::NotFound)
|
||||
};
|
||||
|
||||
let files = get_wup_task_files(pool, false, boss_app_id.to_string(), task_id.to_string(), country, language, None).await;
|
||||
|
||||
let tasksheet = TaskSheet{
|
||||
title_id: task.title_id.clone(),
|
||||
task_id: task.id.clone(),
|
||||
service_status: task.status.clone(),
|
||||
files: TaskSheetFiles{file: files.iter().map(|f| build_file(task.clone(), f, mode == Some("attr".to_string()), &cdn_url)).collect()},
|
||||
};
|
||||
|
||||
let xml = quick_xml::se::to_string(&tasksheet);
|
||||
|
||||
match xml {
|
||||
Ok(xml) => {
|
||||
let xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{}"#, xml);
|
||||
Ok(RawXml(xml))
|
||||
},
|
||||
Err(_) => Err(Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::get("/p01/tasksheet/<id>/<task_id>?<query..>")]
|
||||
pub async fn tasksheet_no_boss_app_id(pool: &State<Pool>, id: &str, task_id: &str, query: QueryParams) -> Result<RawXml<String>, Status> {
|
||||
let pool = pool.inner();
|
||||
let cdn_url = match env::var("BOSS_CDN_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let country = query.c;
|
||||
let language = query.l;
|
||||
let mode = query.mode;
|
||||
|
||||
let boss_app_id = "";
|
||||
|
||||
let task = match get_task(pool, boss_app_id.to_string(), task_id.to_string()).await {
|
||||
Some(task) => task,
|
||||
None => return Err(Status::NotFound)
|
||||
};
|
||||
|
||||
let files = get_wup_task_files(pool, false, boss_app_id.to_string(), task_id.to_string(), country, language, None).await;
|
||||
|
||||
let tasksheet = TaskSheet{
|
||||
title_id: task.title_id.clone(),
|
||||
task_id: task.id.clone(),
|
||||
service_status: task.status.clone(),
|
||||
files: TaskSheetFiles{file: files.iter().map(|f| build_file(task.clone(), f, mode == Some("attr".to_string()), &cdn_url)).collect()},
|
||||
};
|
||||
|
||||
let xml = quick_xml::se::to_string(&tasksheet);
|
||||
|
||||
match xml {
|
||||
Ok(xml) => {
|
||||
let xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{}"#, xml);
|
||||
Ok(RawXml(xml))
|
||||
},
|
||||
Err(_) => Err(Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::get("/p01/tasksheet/<id>/<boss_app_id>/<task_id>/<file_name>?<query..>")]
|
||||
pub async fn tasksheet_file(pool: &State<Pool>, id: &str, boss_app_id: &str, task_id: &str, file_name: &str, query: QueryParams) -> Result<RawXml<String>, Status> {
|
||||
let pool = pool.inner();
|
||||
let cdn_url = match env::var("BOSS_CDN_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => return Err(Status::InternalServerError),
|
||||
};
|
||||
|
||||
let country = query.c;
|
||||
let language = query.l;
|
||||
let mode = query.mode;
|
||||
|
||||
let task = match get_task(pool, boss_app_id.to_string(), task_id.to_string()).await {
|
||||
Some(task) => task,
|
||||
None => return Err(Status::NotFound)
|
||||
};
|
||||
|
||||
let file = match get_wup_task_file(pool, false, boss_app_id.to_string(), task_id.to_string(), file_name.to_string(), country, language, None).await {
|
||||
Some(file) => file,
|
||||
None => return Err(Status::NotFound),
|
||||
};
|
||||
|
||||
let tasksheet = TaskSheet{
|
||||
title_id: task.title_id.clone(),
|
||||
task_id: task.id.clone(),
|
||||
service_status: task.status.clone(),
|
||||
files: TaskSheetFiles{file: vec![build_file(task.clone(), &file, mode == Some("attr".to_string()), &cdn_url)]},
|
||||
};
|
||||
|
||||
let xml = quick_xml::se::to_string(&tasksheet);
|
||||
|
||||
match xml {
|
||||
Ok(xml) => {
|
||||
let xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{}"#, xml);
|
||||
Ok(RawXml(xml))
|
||||
},
|
||||
Err(_) => Err(Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue