Compare commits
8 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
569d87aec4 | |
|
|
c9a91d3d36 | |
|
|
a1b3bbb999 | |
|
|
752ce3f16c | |
|
|
79a2f2839f | |
|
|
c4d12562a1 | |
|
|
eb23143739 | |
|
|
312d6c98cf |
|
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
|
|
@ -103,6 +103,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
|
@ -117,7 +123,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -375,7 +381,7 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time 0.3.23",
|
"time 0.3.41",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -456,6 +462,16 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derivative"
|
name = "derivative"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -525,7 +541,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -685,7 +701,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1112,6 +1128,7 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
name = "memejoin-rs"
|
name = "memejoin-rs"
|
||||||
version = "0.2.2-alpha"
|
version = "0.2.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
|
|
@ -1225,6 +1242,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.1.43"
|
version = "0.1.43"
|
||||||
|
|
@ -1297,7 +1320,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1379,7 +1402,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1450,6 +1473,12 @@ dependencies = [
|
||||||
"universal-hash 0.5.1",
|
"universal-hash 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -1772,9 +1801,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.177"
|
version = "1.0.193"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a"
|
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
@ -1791,13 +1820,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.177"
|
version = "1.0.193"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3"
|
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1829,7 +1858,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1867,7 +1896,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde-value",
|
"serde-value",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"time 0.3.23",
|
"time 0.3.41",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"typemap_rev",
|
"typemap_rev",
|
||||||
|
|
@ -2043,9 +2072,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.27"
|
version = "2.0.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
|
checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2088,7 +2117,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2114,11 +2143,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.23"
|
version = "0.3.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
|
|
@ -2126,16 +2158,17 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.1"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.10"
|
version = "0.2.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2181,7 +2214,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2296,7 +2329,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2539,7 +2572,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2573,7 +2606,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.27",
|
"syn 2.0.32",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,11 +1,20 @@
|
||||||
[package]
|
[package]
|
||||||
name = "memejoin-rs"
|
name = "memejoin-rs"
|
||||||
version = "0.2.2-alpha"
|
version = "0.2.2-alpha"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "memejoin-rs"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "memejoin_rs"
|
||||||
|
path = "src/lib/mod.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.100"
|
||||||
async-trait = "0.1.72"
|
async-trait = "0.1.72"
|
||||||
axum = { version = "0.6.9", features = ["headers", "multipart"] }
|
axum = { version = "0.6.9", features = ["headers", "multipart"] }
|
||||||
axum-extra = { version = "0.7.5", features = ["cookie-private", "cookie"] }
|
axum-extra = { version = "0.7.5", features = ["cookie-private", "cookie"] }
|
||||||
|
|
|
||||||
58
flake.lock
58
flake.lock
|
|
@ -5,29 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710146030,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1705309234,
|
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -38,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1717786204,
|
"lastModified": 1743583204,
|
||||||
"narHash": "sha256-4q0s6m0GUcN7q+Y2DqD27iLvbcd1G50T2lv08kKxkSI=",
|
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "051f920625ab5aabe37c920346e3e69d7d34400e",
|
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -54,11 +36,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706487304,
|
"lastModified": 1744536153,
|
||||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -77,15 +59,14 @@
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1717985971,
|
"lastModified": 1759718104,
|
||||||
"narHash": "sha256-24h/qKp0aeI+Ew13WdRF521kY24PYa5HOvw0mlrABjk=",
|
"narHash": "sha256-TbkLsgdnXHUXR4gOQBmhxkEE9ne+eHmX1chZHWRogy0=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "abfe5b3126b1b7e9e4daafc1c6478d17f0b584e7",
|
"rev": "edea9f33f9a03f615ad3609a40fbcefe0ec835ca",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -108,21 +89,6 @@
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,22 @@
|
||||||
};
|
};
|
||||||
yt-dlp = pkgs.yt-dlp.overrideAttrs (oldAttr: rec {
|
yt-dlp = pkgs.yt-dlp.overrideAttrs (oldAttr: rec {
|
||||||
inherit (oldAttr) name;
|
inherit (oldAttr) name;
|
||||||
version = "2024.05.27";
|
version = "2025.09.26";
|
||||||
src = pkgs.fetchFromGitHub {
|
src = pkgs.fetchFromGitHub {
|
||||||
owner = "yt-dlp";
|
owner = "yt-dlp";
|
||||||
repo = "yt-dlp";
|
repo = "yt-dlp";
|
||||||
rev = "${version}";
|
rev = "${version}";
|
||||||
sha256 = "55zDAMwCJPn5zKrAFw4ogTxxmvjrv4PvhYO7PsHbRo4=";
|
sha256 = "/uzs87Vw+aDNfIJVLOx3C8RyZvWLqjggmnjrOvUX1Eg=";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
local-rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain).override {
|
local-rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain).override {
|
||||||
extensions = [ "rust-analysis" ];
|
extensions = [ "rust-analyzer" "rust-src" ];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
git
|
||||||
local-rust
|
local-rust
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
|
||||||
160
src/db/mod.rs
160
src/db/mod.rs
|
|
@ -249,66 +249,66 @@ impl Database {
|
||||||
intros
|
intros
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_all_user_permissions(
|
// pub(crate) fn get_all_user_permissions(
|
||||||
&self,
|
// &self,
|
||||||
guild_id: u64,
|
// guild_id: u64,
|
||||||
) -> Result<Vec<(String, auth::Permissions)>> {
|
// ) -> Result<Vec<(String, auth::Permissions)>> {
|
||||||
let mut query = self.conn.prepare(
|
// let mut query = self.conn.prepare(
|
||||||
"
|
// "
|
||||||
SELECT
|
// SELECT
|
||||||
username,
|
// username,
|
||||||
permissions
|
// permissions
|
||||||
FROM UserPermission
|
// FROM UserPermission
|
||||||
WHERE
|
// WHERE
|
||||||
guild_id = :guild_id
|
// guild_id = :guild_id
|
||||||
",
|
// ",
|
||||||
)?;
|
// )?;
|
||||||
|
//
|
||||||
|
// let permissions = query
|
||||||
|
// .query_map(
|
||||||
|
// &[
|
||||||
|
// // :vomit:
|
||||||
|
// (":guild_id", &guild_id.to_string()),
|
||||||
|
// ],
|
||||||
|
// |row| Ok((row.get(0)?, auth::Permissions(row.get(1)?))),
|
||||||
|
// )?
|
||||||
|
// .collect::<Result<Vec<(String, auth::Permissions)>>>()?;
|
||||||
|
//
|
||||||
|
// Ok(permissions)
|
||||||
|
// }
|
||||||
|
|
||||||
let permissions = query
|
// pub(crate) fn get_user_permissions(
|
||||||
.query_map(
|
// &self,
|
||||||
&[
|
// username: &str,
|
||||||
// :vomit:
|
// guild_id: u64,
|
||||||
(":guild_id", &guild_id.to_string()),
|
// ) -> Result<auth::Permissions> {
|
||||||
],
|
// self.conn.query_row(
|
||||||
|row| Ok((row.get(0)?, auth::Permissions(row.get(1)?))),
|
// "
|
||||||
)?
|
// SELECT
|
||||||
.collect::<Result<Vec<(String, auth::Permissions)>>>()?;
|
// permissions
|
||||||
|
// FROM UserPermission
|
||||||
|
// WHERE
|
||||||
|
// username = ?1
|
||||||
|
// AND guild_id = ?2
|
||||||
|
// ",
|
||||||
|
// [username, &guild_id.to_string()],
|
||||||
|
// |row| Ok(auth::Permissions(row.get(0)?)),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
Ok(permissions)
|
// pub(crate) fn get_user_app_permissions(&self, username: &str) -> Result<auth::AppPermissions> {
|
||||||
}
|
// self.conn.query_row(
|
||||||
|
// "
|
||||||
pub(crate) fn get_user_permissions(
|
// SELECT
|
||||||
&self,
|
// permissions
|
||||||
username: &str,
|
// FROM UserAppPermission
|
||||||
guild_id: u64,
|
// WHERE
|
||||||
) -> Result<auth::Permissions> {
|
// username = ?1
|
||||||
self.conn.query_row(
|
// ",
|
||||||
"
|
// [username],
|
||||||
SELECT
|
// |row| Ok(auth::AppPermissions(row.get(0)?)),
|
||||||
permissions
|
// )
|
||||||
FROM UserPermission
|
// }
|
||||||
WHERE
|
|
||||||
username = ?1
|
|
||||||
AND guild_id = ?2
|
|
||||||
",
|
|
||||||
[username, &guild_id.to_string()],
|
|
||||||
|row| Ok(auth::Permissions(row.get(0)?)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_user_app_permissions(&self, username: &str) -> Result<auth::AppPermissions> {
|
|
||||||
self.conn.query_row(
|
|
||||||
"
|
|
||||||
SELECT
|
|
||||||
permissions
|
|
||||||
FROM UserAppPermission
|
|
||||||
WHERE
|
|
||||||
username = ?1
|
|
||||||
",
|
|
||||||
[username],
|
|
||||||
|row| Ok(auth::AppPermissions(row.get(0)?)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_guild_channels(&self, guild_id: u64) -> Result<Vec<String>> {
|
pub(crate) fn get_guild_channels(&self, guild_id: u64) -> Result<Vec<String>> {
|
||||||
let mut query = self.conn.prepare(
|
let mut query = self.conn.prepare(
|
||||||
|
|
@ -476,28 +476,29 @@ impl Database {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_user_permission(
|
// pub(crate) fn insert_user_permission(
|
||||||
&self,
|
// &self,
|
||||||
username: &str,
|
// username: &str,
|
||||||
guild_id: u64,
|
// guild_id: u64,
|
||||||
permissions: auth::Permissions,
|
// permissions: auth::Permissions,
|
||||||
) -> Result<()> {
|
// ) -> Result<()> {
|
||||||
let affected = self.conn.execute(
|
// let affected = self.conn.execute(
|
||||||
"
|
// "
|
||||||
INSERT INTO
|
// INSERT INTO
|
||||||
UserPermission (username, guild_id, permissions)
|
// UserPermission (username, guild_id, permissions)
|
||||||
VALUES (?1, ?2, ?3)
|
// VALUES (?1, ?2, ?3)
|
||||||
ON CONFLICT(username, guild_id) DO UPDATE SET permissions = ?3",
|
// ON CONFLICT(username, guild_id) DO UPDATE SET permissions = ?3",
|
||||||
[username, &guild_id.to_string(), &permissions.0.to_string()],
|
// [username, &guild_id.to_string(), &permissions.0.to_string()],
|
||||||
)?;
|
// )?;
|
||||||
|
//
|
||||||
if affected < 1 {
|
// if affected < 1 {
|
||||||
warn!("no rows affected when attempting to insert user permissions");
|
// warn!("no rows affected when attempting to insert user permissions");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Ok(())
|
// Ok(())
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
/*
|
||||||
pub(crate) fn insert_user_app_permission(
|
pub(crate) fn insert_user_app_permission(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
|
|
@ -518,6 +519,7 @@ impl Database {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub fn delete_user_intro(
|
pub fn delete_user_intro(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use enum_iterator::Sequence;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::routes::Error;
|
#[derive(Clone)]
|
||||||
|
pub struct DiscordSecret {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub bot_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub(crate) struct Discord {
|
pub(crate) struct Discord {
|
||||||
|
|
@ -14,11 +16,19 @@ pub(crate) struct Discord {
|
||||||
pub(crate) scope: String,
|
pub(crate) scope: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/*
|
||||||
pub(crate) struct DiscordSecret {
|
use std::str::FromStr;
|
||||||
pub(crate) client_id: String,
|
|
||||||
pub(crate) client_secret: String,
|
use enum_iterator::Sequence;
|
||||||
pub(crate) bot_token: String,
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct Discord {
|
||||||
|
pub(crate) access_token: String,
|
||||||
|
pub(crate) token_type: String,
|
||||||
|
pub(crate) expires_in: usize,
|
||||||
|
pub(crate) refresh_token: String,
|
||||||
|
pub(crate) scope: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -143,3 +153,4 @@ impl FromStr for Permission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::{
|
||||||
|
models::{self, guild::IntroId},
|
||||||
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ports::AuthService;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
impersonated_username: String,
|
||||||
|
wrapped_service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
pub fn new(wrapped_service: S, impersonated_username: String) -> Self {
|
||||||
|
Self {
|
||||||
|
wrapped_service,
|
||||||
|
impersonated_username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> IntroToolService for DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
async fn needs_setup(&self) -> bool {
|
||||||
|
self.wrapped_service.needs_setup().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate_user<A: AuthService>(
|
||||||
|
&self,
|
||||||
|
params: A::Params,
|
||||||
|
) -> Result<models::guild::ApiToken, models::guild::AutheticateUserError<A>> {
|
||||||
|
self.wrapped_service.authenticate_user(params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: impl Into<models::guild::GuildId> + Send,
|
||||||
|
) -> Result<models::guild::Guild, models::guild::GetGuildError> {
|
||||||
|
self.wrapped_service.get_guild(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guilds(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<models::guild::GuildRef>, models::guild::GetGuildError> {
|
||||||
|
self.wrapped_service.get_guilds().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_users(
|
||||||
|
&self,
|
||||||
|
guild_id: models::guild::GuildId,
|
||||||
|
) -> Result<Vec<models::guild::User>, models::guild::GetUserError> {
|
||||||
|
self.wrapped_service.get_guild_users(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: models::guild::GuildId,
|
||||||
|
) -> Result<Vec<models::guild::Intro>, models::guild::GetIntroError> {
|
||||||
|
self.wrapped_service.get_guild_intros(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<models::guild::User, models::guild::GetUserError> {
|
||||||
|
self.wrapped_service.get_user(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<Vec<models::guild::GuildRef>, models::guild::GetGuildError> {
|
||||||
|
self.wrapped_service.get_user_guilds(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
_api_key: &str,
|
||||||
|
) -> Result<models::guild::User, models::guild::GetUserError> {
|
||||||
|
let user = self
|
||||||
|
.wrapped_service
|
||||||
|
.get_user(&self.impersonated_username)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(models::guild::User::new(
|
||||||
|
self.impersonated_username.clone(),
|
||||||
|
"testApiKey".into(),
|
||||||
|
Utc::now().naive_utc() + Duration::days(1),
|
||||||
|
"testDiscordToken".into(),
|
||||||
|
Utc::now().naive_utc() + Duration::days(1),
|
||||||
|
)
|
||||||
|
.with_channel_intros(user.intros().clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_user_intro(
|
||||||
|
&self,
|
||||||
|
req: models::guild::AddIntroToUserRequest,
|
||||||
|
) -> Result<(), models::guild::AddIntroToUserError> {
|
||||||
|
self.wrapped_service.set_user_intro(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh_user_token(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<String, models::guild::GetUserError> {
|
||||||
|
self.wrapped_service.refresh_user_token(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_guild(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateGuildRequest,
|
||||||
|
) -> Result<models::guild::Guild, models::guild::CreateGuildError> {
|
||||||
|
self.wrapped_service.create_guild(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateUserRequest,
|
||||||
|
) -> Result<models::guild::User, models::guild::CreateUserError> {
|
||||||
|
self.wrapped_service.create_user(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user_to_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: models::guild::GuildId,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<(), models::guild::AddUserToGuildError> {
|
||||||
|
self.wrapped_service
|
||||||
|
.add_user_to_guild(guild_id, username)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateChannelRequest,
|
||||||
|
) -> Result<models::guild::Channel, models::guild::CreateChannelError> {
|
||||||
|
self.wrapped_service.create_channel(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
req: models::guild::AddIntroToGuildRequest,
|
||||||
|
) -> Result<IntroId, models::guild::AddIntroToGuildError> {
|
||||||
|
self.wrapped_service.add_intro_to_guild(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod debug_service;
|
||||||
|
pub mod models;
|
||||||
|
pub mod ports;
|
||||||
|
pub mod service;
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::ports::AuthService;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ApiToken(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct GuildId(u64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExternalGuildId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct UserName(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ChannelName(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct IntroId(i32);
|
||||||
|
|
||||||
|
impl From<ApiToken> for Cow<'_, str> {
|
||||||
|
fn from(value: ApiToken) -> Self {
|
||||||
|
Cow::Owned(value.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ApiToken {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for GuildId {
|
||||||
|
fn from(id: u64) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for ExternalGuildId {
|
||||||
|
fn from(id: u64) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for IntroId {
|
||||||
|
fn from(id: i32) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for UserName {
|
||||||
|
fn from(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ChannelName {
|
||||||
|
fn from(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for UserName {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for ChannelName {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for GuildId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UserName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ChannelName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IntroId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Guild {
|
||||||
|
guild: GuildRef,
|
||||||
|
|
||||||
|
channels: Vec<Channel>,
|
||||||
|
users: Vec<User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GuildRef {
|
||||||
|
id: GuildId,
|
||||||
|
name: String,
|
||||||
|
sound_delay: u32,
|
||||||
|
external_id: ExternalGuildId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuildRef {
|
||||||
|
pub fn id(&self) -> GuildId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn external_id(&self) -> ExternalGuildId {
|
||||||
|
self.external_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuildRef {
|
||||||
|
pub fn new(id: GuildId, name: String, sound_delay: u32, external_id: ExternalGuildId) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sound_delay,
|
||||||
|
external_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Guild {
|
||||||
|
pub fn new(id: GuildId, name: String, sound_delay: u32, external_id: ExternalGuildId) -> Self {
|
||||||
|
Self {
|
||||||
|
guild: GuildRef {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sound_delay,
|
||||||
|
external_id,
|
||||||
|
},
|
||||||
|
channels: vec![],
|
||||||
|
users: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> GuildId {
|
||||||
|
self.guild.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.guild.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn users(&self) -> &[User] {
|
||||||
|
&self.users
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channels(&self) -> &[Channel] {
|
||||||
|
&self.channels
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_users(self, users: Vec<User>) -> Self {
|
||||||
|
Self { users, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_channels(self, channels: Vec<Channel>) -> Self {
|
||||||
|
Self { channels, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct User {
|
||||||
|
name: UserName,
|
||||||
|
|
||||||
|
api_key: String,
|
||||||
|
api_key_expires_at: NaiveDateTime,
|
||||||
|
discord_token: String,
|
||||||
|
discord_token_expires_at: NaiveDateTime,
|
||||||
|
|
||||||
|
channel_intros: HashMap<(GuildId, ChannelName), Vec<Intro>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<UserName>,
|
||||||
|
api_key: String,
|
||||||
|
api_key_expires_at: NaiveDateTime,
|
||||||
|
discord_token: String,
|
||||||
|
discord_token_expires_at: NaiveDateTime,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
api_key,
|
||||||
|
api_key_expires_at,
|
||||||
|
discord_token,
|
||||||
|
discord_token_expires_at,
|
||||||
|
channel_intros: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intros(&self) -> &HashMap<(GuildId, ChannelName), Vec<Intro>> {
|
||||||
|
&self.channel_intros
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_key(&self) -> &str {
|
||||||
|
&self.api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_key_expires_at(&self) -> NaiveDateTime {
|
||||||
|
self.api_key_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discord_token_expires_at(&self) -> NaiveDateTime {
|
||||||
|
self.discord_token_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_channel_intros(
|
||||||
|
self,
|
||||||
|
channel_intros: HashMap<(GuildId, ChannelName), Vec<Intro>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
channel_intros,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Channel {
|
||||||
|
name: ChannelName,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
pub fn new(name: ChannelName) -> Self {
|
||||||
|
Self { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &ChannelName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Intro {
|
||||||
|
id: IntroId,
|
||||||
|
|
||||||
|
name: String,
|
||||||
|
filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Intro {
|
||||||
|
pub fn new(id: IntroId, name: String, filename: String) -> Self {
|
||||||
|
Self { id, name, filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> IntroId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateGuildRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub sound_delay: u32,
|
||||||
|
pub external_id: ExternalGuildId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateUserRequest {
|
||||||
|
pub user: UserName,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateChannelRequest {
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
pub channel_name: ChannelName,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddIntroToGuildRequest {
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
pub name: String,
|
||||||
|
pub volume: i32,
|
||||||
|
|
||||||
|
pub data: IntroRequestData,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum IntroRequestData {
|
||||||
|
Data(Vec<u8>),
|
||||||
|
Url(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddIntroToUserRequest {
|
||||||
|
pub user: UserName,
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
pub channel_name: ChannelName,
|
||||||
|
pub intro_id: IntroId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateGuildError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateUserError {
|
||||||
|
#[error("Could not get user")]
|
||||||
|
CouldNotGetUser(#[from] GetUserError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateChannelError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AddUserToGuildError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AddIntroToGuildError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AddIntroToUserError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetGuildError {
|
||||||
|
#[error("Guild not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Could not fetch guild users")]
|
||||||
|
CouldNotFetchUsers(#[from] GetUserError),
|
||||||
|
|
||||||
|
#[error("Could not fetch guild channels")]
|
||||||
|
CouldNotFetchChannels(#[from] GetChannelError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetUserError {
|
||||||
|
#[error("User not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Could not fetch user guilds")]
|
||||||
|
CouldNotFetchGuilds(#[from] Box<GetGuildError>),
|
||||||
|
|
||||||
|
#[error("Could not fetch user channel intros")]
|
||||||
|
CouldNotFetchChannelIntros(#[from] GetIntroError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetChannelError {
|
||||||
|
#[error("Channel not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetIntroError {
|
||||||
|
#[error("Intro not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AutheticateUserError<A: AuthService> {
|
||||||
|
#[error("Could not fetch guild")]
|
||||||
|
CouldNotFetchGuild(#[from] GetGuildError),
|
||||||
|
|
||||||
|
#[error("Could not create user")]
|
||||||
|
CouldNotCreateUser(#[from] CreateUserError),
|
||||||
|
|
||||||
|
#[error("Could not fetch guild user")]
|
||||||
|
CouldNotFetchUser(#[from] GetUserError),
|
||||||
|
|
||||||
|
#[error("Could not add user to guild")]
|
||||||
|
CouldNotAddUserToGuild(#[from] AddUserToGuildError),
|
||||||
|
|
||||||
|
#[error("User not part of instance's guilds")]
|
||||||
|
UserNotPartOfInstanceGuilds,
|
||||||
|
|
||||||
|
#[error("Error authenticating user")]
|
||||||
|
ExternalError(A::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod guild;
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
use std::{collections::HashMap, future::Future};
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::models::guild::{
|
||||||
|
AddUserToGuildError, AutheticateUserError, ExternalGuildId, UserName,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::models::guild::{
|
||||||
|
AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest,
|
||||||
|
ApiToken, Channel, ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError,
|
||||||
|
CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError,
|
||||||
|
GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, IntroId, User,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait IntroToolService: Send + Sync + Clone + 'static {
|
||||||
|
fn needs_setup(&self) -> impl Future<Output = bool> + Send;
|
||||||
|
|
||||||
|
fn authenticate_user<A: AuthService>(
|
||||||
|
&self,
|
||||||
|
params: A::Params,
|
||||||
|
) -> impl Future<Output = Result<ApiToken, AutheticateUserError<A>>> + Send;
|
||||||
|
|
||||||
|
fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: impl Into<GuildId> + Send,
|
||||||
|
) -> impl Future<Output = Result<Guild, GetGuildError>> + Send;
|
||||||
|
fn get_guilds(&self) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
fn get_guild_users(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<User>, GetUserError>> + Send;
|
||||||
|
fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<Intro>, GetIntroError>> + Send;
|
||||||
|
fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
api_key: &str,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn set_user_intro(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToUserRequest,
|
||||||
|
) -> impl Future<Output = Result<(), AddIntroToUserError>> + Send;
|
||||||
|
|
||||||
|
fn refresh_user_token(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
) -> impl Future<Output = Result<String, GetUserError>> + Send;
|
||||||
|
|
||||||
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
||||||
|
|
||||||
|
fn create_user(
|
||||||
|
&self,
|
||||||
|
req: CreateUserRequest,
|
||||||
|
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
||||||
|
|
||||||
|
fn add_user_to_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
username: &str,
|
||||||
|
) -> impl Future<Output = Result<(), AddUserToGuildError>> + Send;
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: CreateChannelRequest,
|
||||||
|
) -> Result<Channel, CreateChannelError>;
|
||||||
|
|
||||||
|
fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToGuildRequest,
|
||||||
|
) -> impl Future<Output = Result<IntroId, AddIntroToGuildError>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntroToolRepository: Send + Sync + Clone + 'static {
|
||||||
|
fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Guild, GetGuildError>> + Send;
|
||||||
|
fn get_guilds(&self) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
fn get_guild_count(&self) -> impl Future<Output = Result<usize, GetGuildError>> + Send;
|
||||||
|
|
||||||
|
fn get_guild_users(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<User>, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn get_guild_channels(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<Channel>, GetChannelError>> + Send;
|
||||||
|
fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<Intro>, GetIntroError>> + Send;
|
||||||
|
|
||||||
|
fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_channel_intros(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<HashMap<(GuildId, ChannelName), Vec<Intro>>, GetIntroError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
api_key: &str,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn set_user_api_key(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
api_key: &str,
|
||||||
|
expires_at: NaiveDateTime,
|
||||||
|
) -> impl Future<Output = Result<(), GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn set_user_intro(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToUserRequest,
|
||||||
|
) -> impl Future<Output = Result<(), AddIntroToUserError>> + Send;
|
||||||
|
|
||||||
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
||||||
|
|
||||||
|
fn create_user(
|
||||||
|
&self,
|
||||||
|
req: CreateUserRequest,
|
||||||
|
) -> impl Future<Output = Result<(), CreateUserError>> + Send;
|
||||||
|
|
||||||
|
fn add_user_to_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
username: &str,
|
||||||
|
) -> impl Future<Output = Result<(), AddUserToGuildError>> + Send;
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: CreateChannelRequest,
|
||||||
|
) -> Result<Channel, CreateChannelError>;
|
||||||
|
|
||||||
|
fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
guild_id: GuildId,
|
||||||
|
filename: String,
|
||||||
|
) -> impl Future<Output = Result<IntroId, AddIntroToGuildError>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ExternalUser: Send + Sync + Clone + 'static {
|
||||||
|
fn external_token(&self) -> &str;
|
||||||
|
fn username(&self) -> UserName;
|
||||||
|
fn guilds(&self) -> impl Iterator<Item = ExternalGuildId>;
|
||||||
|
}
|
||||||
|
pub trait AuthService: Send + Sync + Clone + 'static {
|
||||||
|
type Params: Send;
|
||||||
|
type User: ExternalUser + Send;
|
||||||
|
type Error: std::error::Error + Send;
|
||||||
|
|
||||||
|
fn authenticate_user(
|
||||||
|
params: Self::Params,
|
||||||
|
) -> impl Future<Output = Result<Self::User, Self::Error>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RemoteAudioFetcher: Send + Sync + Clone + 'static {
|
||||||
|
fn fetch_remote_audio(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> impl Future<Output = Result<String, anyhow::Error>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LocalAudioFetcher: Send + Sync + Clone + 'static {
|
||||||
|
fn save_local_audio(
|
||||||
|
&self,
|
||||||
|
bytes: &[u8],
|
||||||
|
name: &str,
|
||||||
|
) -> impl Future<Output = Result<String, anyhow::Error>> + Send;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use iter_tools::Itertools;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::{
|
||||||
|
models::guild::{
|
||||||
|
self, ApiToken, AutheticateUserError, CreateUserRequest, GetUserError, GuildId, IntroId,
|
||||||
|
User,
|
||||||
|
},
|
||||||
|
ports::{
|
||||||
|
ExternalUser, IntroToolRepository, IntroToolService, LocalAudioFetcher, RemoteAudioFetcher,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ports::AuthService;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Service<R, RA, LA>
|
||||||
|
where
|
||||||
|
R: IntroToolRepository,
|
||||||
|
RA: RemoteAudioFetcher,
|
||||||
|
LA: LocalAudioFetcher,
|
||||||
|
{
|
||||||
|
repo: R,
|
||||||
|
remote_audio_fetcher: RA,
|
||||||
|
local_audio_fetcher: LA,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, RA, LA> Service<R, RA, LA>
|
||||||
|
where
|
||||||
|
R: IntroToolRepository,
|
||||||
|
RA: RemoteAudioFetcher,
|
||||||
|
LA: LocalAudioFetcher,
|
||||||
|
{
|
||||||
|
pub fn new(repo: R, remote_audio_fetcher: RA, local_audio_fetcher: LA) -> Self {
|
||||||
|
Self {
|
||||||
|
repo,
|
||||||
|
remote_audio_fetcher,
|
||||||
|
local_audio_fetcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, RA, LA> IntroToolService for Service<R, RA, LA>
|
||||||
|
where
|
||||||
|
R: IntroToolRepository,
|
||||||
|
RA: RemoteAudioFetcher,
|
||||||
|
LA: LocalAudioFetcher,
|
||||||
|
{
|
||||||
|
async fn needs_setup(&self) -> bool {
|
||||||
|
let Ok(guild_count) = self.repo.get_guild_count().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
guild_count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate_user<A: AuthService>(
|
||||||
|
&self,
|
||||||
|
params: A::Params,
|
||||||
|
) -> Result<ApiToken, AutheticateUserError<A>> {
|
||||||
|
let external_user = A::authenticate_user(params)
|
||||||
|
.await
|
||||||
|
.map_err(AutheticateUserError::ExternalError)?;
|
||||||
|
|
||||||
|
let guilds = self.get_guilds().await?;
|
||||||
|
let external_user_guilds = guilds
|
||||||
|
.iter()
|
||||||
|
.filter(|guild| external_user.guilds().contains(&guild.external_id()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if external_user_guilds.is_empty() {
|
||||||
|
return Err(AutheticateUserError::UserNotPartOfInstanceGuilds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = match self.get_user(external_user.username()).await {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(GetUserError::NotFound) => None,
|
||||||
|
|
||||||
|
Err(err) => return Err(AutheticateUserError::CouldNotFetchUser(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match user {
|
||||||
|
Some(user) => {
|
||||||
|
self.refresh_user_token(user.name()).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.create_user(CreateUserRequest {
|
||||||
|
user: external_user.username().clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self.get_user(external_user.username()).await?;
|
||||||
|
let user_guilds = self.get_user_guilds(user.name()).await?;
|
||||||
|
|
||||||
|
let guilds_to_add_user =
|
||||||
|
user_guilds
|
||||||
|
.iter()
|
||||||
|
.map(|guild| guild.id())
|
||||||
|
.filter(|user_guild_id| {
|
||||||
|
external_user_guilds
|
||||||
|
.iter()
|
||||||
|
.map(|external_guild| external_guild.id())
|
||||||
|
.contains(user_guild_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
for guild in guilds_to_add_user {
|
||||||
|
self.add_user_to_guild(guild, user.name()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user.api_key().to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: impl Into<GuildId>,
|
||||||
|
) -> Result<guild::Guild, guild::GetGuildError> {
|
||||||
|
self.repo.get_guild(guild_id.into()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guilds(&self) -> Result<Vec<guild::GuildRef>, guild::GetGuildError> {
|
||||||
|
self.repo.get_guilds().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_users(&self, guild_id: GuildId) -> Result<Vec<User>, GetUserError> {
|
||||||
|
self.repo.get_guild_users(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<Vec<guild::Intro>, guild::GetIntroError> {
|
||||||
|
self.repo.get_guild_intros(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<guild::User, guild::GetUserError> {
|
||||||
|
self.repo.get_user(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<Vec<guild::GuildRef>, guild::GetGuildError> {
|
||||||
|
self.repo.get_user_guilds(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_api_key(&self, api_key: &str) -> Result<User, GetUserError> {
|
||||||
|
self.repo.get_user_from_api_key(api_key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_user_intro(
|
||||||
|
&self,
|
||||||
|
req: guild::AddIntroToUserRequest,
|
||||||
|
) -> Result<(), guild::AddIntroToUserError> {
|
||||||
|
self.repo.set_user_intro(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh_user_token(&self, username: &str) -> Result<String, GetUserError> {
|
||||||
|
let user = self.get_user(username).await?;
|
||||||
|
|
||||||
|
let user_token = if user.api_key_expires_at() >= Utc::now().naive_utc() {
|
||||||
|
user.api_key().to_string()
|
||||||
|
} else {
|
||||||
|
Uuid::new_v4().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let expires_at = Utc::now().naive_utc() + Duration::weeks(4);
|
||||||
|
|
||||||
|
self.repo
|
||||||
|
.set_user_api_key(username, &user_token, expires_at)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_guild(
|
||||||
|
&self,
|
||||||
|
req: guild::CreateGuildRequest,
|
||||||
|
) -> Result<guild::Guild, guild::CreateGuildError> {
|
||||||
|
self.repo.create_guild(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
&self,
|
||||||
|
req: guild::CreateUserRequest,
|
||||||
|
) -> Result<guild::User, guild::CreateUserError> {
|
||||||
|
let username = req.user.clone();
|
||||||
|
|
||||||
|
self.repo.create_user(req).await?;
|
||||||
|
|
||||||
|
Ok(self.get_user(username.as_ref()).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user_to_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<(), guild::AddUserToGuildError> {
|
||||||
|
self.repo.add_user_to_guild(guild_id, username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: guild::CreateChannelRequest,
|
||||||
|
) -> Result<guild::Channel, guild::CreateChannelError> {
|
||||||
|
self.repo.create_channel(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
req: guild::AddIntroToGuildRequest,
|
||||||
|
) -> Result<IntroId, guild::AddIntroToGuildError> {
|
||||||
|
let file_name = match &req.data {
|
||||||
|
guild::IntroRequestData::Data(bytes) => {
|
||||||
|
self.local_audio_fetcher
|
||||||
|
.save_local_audio(bytes, Uuid::new_v4().to_string().as_str())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
guild::IntroRequestData::Url(url) => {
|
||||||
|
self.remote_audio_fetcher
|
||||||
|
.fetch_remote_audio(url, Uuid::new_v4().to_string().as_str())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.repo
|
||||||
|
.add_intro_to_guild(&req.name, req.guild_id, file_name)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod intro_tool;
|
||||||
|
|
@ -63,6 +63,7 @@ pub enum Tag {
|
||||||
Header6,
|
Header6,
|
||||||
Strong,
|
Strong,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
|
Blockquote,
|
||||||
JustText,
|
JustText,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ impl Tag {
|
||||||
Self::Header6 => "h6",
|
Self::Header6 => "h6",
|
||||||
Self::Strong => "strong",
|
Self::Strong => "strong",
|
||||||
Self::Paragraph => "paragraph",
|
Self::Paragraph => "paragraph",
|
||||||
|
Self::Blockquote => "blockquote",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod http;
|
||||||
|
pub mod response;
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
mod handlers;
|
||||||
|
pub(super) mod page;
|
||||||
|
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::request::Parts,
|
||||||
|
response::Redirect,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use chrono::Utc;
|
||||||
|
use reqwest::Method;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth,
|
||||||
|
domain::intro_tool::{models::guild::User, ports::IntroToolService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ApiState<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
intro_tool_service: Arc<S>,
|
||||||
|
|
||||||
|
pub secrets: auth::DiscordSecret,
|
||||||
|
pub origin: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::async_trait]
|
||||||
|
impl<S: IntroToolService> FromRequestParts<ApiState<S>> for User {
|
||||||
|
type Rejection = Redirect;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
Parts { headers, .. }: &mut Parts,
|
||||||
|
state: &ApiState<S>,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let jar = CookieJar::from_headers(headers);
|
||||||
|
|
||||||
|
if let Some(token) = jar.get("access_token") {
|
||||||
|
match state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_from_api_key(token.value())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(user) => {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
if user.api_key_expires_at() < now {
|
||||||
|
//|| user.discord_token_expires_at() < now {
|
||||||
|
tracing::error!("user token expired at: {}", user.api_key_expires_at());
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
} else {
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?err, "failed to authenticate user");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpServer {
|
||||||
|
make_service: axum::routing::IntoMakeService<axum::Router>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpServer {
|
||||||
|
pub fn new(
|
||||||
|
intro_tool_service: impl IntroToolService,
|
||||||
|
secrets: auth::DiscordSecret,
|
||||||
|
origin: String,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let state = ApiState {
|
||||||
|
intro_tool_service: Arc::new(intro_tool_service),
|
||||||
|
secrets,
|
||||||
|
origin: origin.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let router = routes()
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin([origin.parse().unwrap()])
|
||||||
|
.allow_headers(tower_http::cors::Any)
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::DELETE]),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
make_service: router.into_make_service(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self) {
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], 8100));
|
||||||
|
info!("socket listening on {addr}");
|
||||||
|
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(self.make_service)
|
||||||
|
.await
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn routes<S>() -> axum::Router<ApiState<S>>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
axum::Router::<ApiState<S>>::new()
|
||||||
|
.route("/", get(page::home))
|
||||||
|
.route("/login", get(page::login))
|
||||||
|
.route("/guild/:guild_id", get(page::guild_dashboard))
|
||||||
|
.route("/v2/intros/:guild/add", get(handlers::add_guild_intro))
|
||||||
|
.route(
|
||||||
|
"/v2/intros/:guild/upload",
|
||||||
|
post(handlers::upload_guild_intro),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v2/intros/add/:guild_id/:channel",
|
||||||
|
post(handlers::set_user_intro),
|
||||||
|
)
|
||||||
|
.route("/v2/auth", get(page::auth))
|
||||||
|
|
||||||
|
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
||||||
|
// .route(
|
||||||
|
// "/guild/:guild_id/add_channel",
|
||||||
|
// post(routes::guild_add_channel),
|
||||||
|
// )
|
||||||
|
// .route(
|
||||||
|
// "/guild/:guild_id/permissions/update",
|
||||||
|
// post(routes::update_guild_permissions),
|
||||||
|
// )
|
||||||
|
// .route(
|
||||||
|
// "/v2/intros/remove/:guild_id/:channel",
|
||||||
|
// post(routes::v2_remove_intro_from_user),
|
||||||
|
// )
|
||||||
|
// .route("/v2/intros/:guild/add", get(routes::v2_add_guild_intro))
|
||||||
|
// .route(
|
||||||
|
// "/v2/intros/:guild/upload",
|
||||||
|
// post(routes::v2_upload_guild_intro),
|
||||||
|
// )
|
||||||
|
// .route("/health", get(routes::health))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Multipart, Path, Query, State},
|
||||||
|
http::{HeaderMap, HeaderValue},
|
||||||
|
response::Html,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::intro_tool::{
|
||||||
|
models::guild::{
|
||||||
|
AddIntroToGuildRequest, AddIntroToUserRequest, ChannelName, GuildId, IntroRequestData,
|
||||||
|
User, UserName,
|
||||||
|
},
|
||||||
|
ports::IntroToolService,
|
||||||
|
},
|
||||||
|
htmx::Build,
|
||||||
|
inbound::{
|
||||||
|
http::{page, ApiState},
|
||||||
|
response::{ApiError, ErrorAsRedirect},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
trait FromApi<T, P>: Sized {
|
||||||
|
async fn from_api(value: T, params: P) -> Result<Self, ApiError>;
|
||||||
|
}
|
||||||
|
trait IntoDomain<T, P> {
|
||||||
|
async fn into_domain(self, params: P) -> Result<T, ApiError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O: FromApi<I, P>, P> IntoDomain<O, P> for I {
|
||||||
|
async fn into_domain(self, params: P) -> Result<O, ApiError> {
|
||||||
|
O::from_api(self, params).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi<HashMap<String, String>, GuildId> for AddIntroToGuildRequest {
|
||||||
|
async fn from_api(value: HashMap<String, String>, params: GuildId) -> Result<Self, ApiError> {
|
||||||
|
let Some(url) = value.get("url") else {
|
||||||
|
return Err(ApiError::bad_request("url is required"));
|
||||||
|
};
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("url cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name) = value.get("name") else {
|
||||||
|
return Err(ApiError::bad_request("name is required"));
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guild_id: params,
|
||||||
|
name: name.to_string(),
|
||||||
|
volume: 0,
|
||||||
|
data: IntroRequestData::Url(url.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi<Multipart, GuildId> for AddIntroToGuildRequest {
|
||||||
|
async fn from_api(mut form_data: Multipart, params: GuildId) -> Result<Self, ApiError> {
|
||||||
|
let mut name = None;
|
||||||
|
let mut file = None;
|
||||||
|
|
||||||
|
while let Ok(Some(field)) = form_data.next_field().await {
|
||||||
|
let Some(field_name) = field.name() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("name") {
|
||||||
|
name = Some(field.text().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected text for name: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("file") {
|
||||||
|
file = Some(field.bytes().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected bytes for file: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name) = name else {
|
||||||
|
return Err(ApiError::bad_request("name is required"));
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = file else {
|
||||||
|
return Err(ApiError::bad_request("file is required"));
|
||||||
|
};
|
||||||
|
if file.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("file cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guild_id: params,
|
||||||
|
name: name.to_string(),
|
||||||
|
volume: 0,
|
||||||
|
data: IntroRequestData::Data(file.to_vec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi<Multipart, (GuildId, UserName, ChannelName)> for AddIntroToUserRequest {
|
||||||
|
async fn from_api(
|
||||||
|
mut value: Multipart,
|
||||||
|
(guild_id, user, channel_name): (GuildId, UserName, ChannelName),
|
||||||
|
) -> Result<Self, ApiError> {
|
||||||
|
let intro_id = value
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|err| ApiError::bad_request(format!("expected intro id: {err:?}")))?
|
||||||
|
.ok_or(ApiError::bad_request("intro id is required"))?
|
||||||
|
.name()
|
||||||
|
.ok_or(ApiError::bad_request("intro id is required"))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|err| ApiError::bad_request(format!("invalid intro id: {err:?}")))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
user,
|
||||||
|
guild_id,
|
||||||
|
channel_name,
|
||||||
|
intro_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn add_guild_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<HeaderMap, ApiError> {
|
||||||
|
let req = params.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intro_tool_service.add_intro_to_guild(req).await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn upload_guild_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
user: User,
|
||||||
|
form_data: Multipart,
|
||||||
|
) -> Result<HeaderMap, ApiError> {
|
||||||
|
let req = form_data.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intro_tool_service.add_intro_to_guild(req).await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn set_user_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path((guild_id, channel)): Path<(u64, String)>,
|
||||||
|
user: User,
|
||||||
|
form_data: Multipart,
|
||||||
|
) -> Result<Html<String>, ApiError> {
|
||||||
|
let req = form_data
|
||||||
|
.into_domain((
|
||||||
|
guild_id.into(),
|
||||||
|
user.name().to_string().into(),
|
||||||
|
channel.clone().into(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if channel exists
|
||||||
|
|
||||||
|
state.intro_tool_service.set_user_intro(req).await?;
|
||||||
|
let user = state.intro_tool_service.get_user(user.name()).await?;
|
||||||
|
|
||||||
|
let guild_intros = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_guild_intros(guild_id.into())
|
||||||
|
.await?;
|
||||||
|
let intros = user
|
||||||
|
.intros()
|
||||||
|
.get(&(guild.id(), channel.clone().into()))
|
||||||
|
.map(|intros| intros.iter())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
page::channel_intro_selector(
|
||||||
|
&state.origin,
|
||||||
|
guild_id,
|
||||||
|
&channel.into(),
|
||||||
|
intros,
|
||||||
|
guild_intros.iter(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
response::{Html, Redirect},
|
||||||
|
};
|
||||||
|
use axum_extra::extract::{CookieJar, cookie::Cookie};
|
||||||
|
use reqwest::Url;
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth,
|
||||||
|
domain::intro_tool::{
|
||||||
|
models::guild::{ChannelName, GuildRef, Intro, User},
|
||||||
|
ports::IntroToolService,
|
||||||
|
},
|
||||||
|
htmx::{Build, HtmxBuilder, Tag},
|
||||||
|
inbound::{
|
||||||
|
http::ApiState,
|
||||||
|
response::{ApiError, ErrorAsRedirect, PageError},
|
||||||
|
},
|
||||||
|
outbound::discord::{DiscordAuthParams, DiscordService},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn home<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: Option<User>,
|
||||||
|
) -> Result<impl axum::response::IntoResponse, Redirect> {
|
||||||
|
if let Some(user) = user {
|
||||||
|
let needs_setup = state.intro_tool_service.needs_setup().await;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "login")?;
|
||||||
|
|
||||||
|
// TODO: get user app permissions
|
||||||
|
// TODO: check if user can add guilds
|
||||||
|
// TODO: fetch guilds from discord
|
||||||
|
|
||||||
|
let can_add_guild = false;
|
||||||
|
let discord_guilds: Vec<GuildRef> = vec![];
|
||||||
|
|
||||||
|
let guild_list = if needs_setup {
|
||||||
|
// TODO:
|
||||||
|
// HtmxBuilder::new(Tag::Empty).builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder_text(Tag::Header2, "Select a Guild to setup")
|
||||||
|
// .push_builder(setup_guild_list(&state.origin, &discord_guilds))
|
||||||
|
// })
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
HtmxBuilder::new(Tag::Empty).builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder_text(Tag::Header2, "Choose a Guild")
|
||||||
|
.push_builder(guild_list(&state.origin, user_guilds.iter()))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
page_header("MemeJoin - Home")
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.push_builder(guild_list)
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// let mut b = b.push_builder(guild_list);
|
||||||
|
//
|
||||||
|
// if !needs_setup && can_add_guild && !discord_guilds.is_empty() {
|
||||||
|
// b = b
|
||||||
|
// .attribute("class", "container")
|
||||||
|
// .builder_text(Tag::Header2, "Add a Guild")
|
||||||
|
// .push_builder(setup_guild_list(&state.origin, &discord_guilds));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// b
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: Option<User>,
|
||||||
|
) -> Result<Html<String>, Redirect> {
|
||||||
|
if user.is_some() {
|
||||||
|
Err(Redirect::to(&format!("{}/", state.origin)))
|
||||||
|
} else {
|
||||||
|
let authorize_uri = format!(
|
||||||
|
"https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}/v2/auth&response_type=code&scope=guilds.members.read+guilds+identify",
|
||||||
|
state.secrets.client_id, state.origin
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
HtmxBuilder::new(Tag::Html)
|
||||||
|
.push_builder(page_header("MemeJoin - Dashboard"))
|
||||||
|
.builder(Tag::Nav, |b| {
|
||||||
|
b.builder(Tag::HeaderGroup, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Header1, |b| b.text("MemeJoin - A bot for user intros"))
|
||||||
|
.builder_text(Tag::Header6, "salad")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.builder(Tag::Main, |b| {
|
||||||
|
b.attribute("class", "container").builder(Tag::Anchor, |b| {
|
||||||
|
b.attribute("role", "button")
|
||||||
|
.text("Login with Discord")
|
||||||
|
.attribute("href", &authorize_uri)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn guild_dashboard<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: User,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
) -> Result<Html<String>, Redirect> {
|
||||||
|
let guild = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_guild(guild_id)
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "login")?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "login")?;
|
||||||
|
let guild_intros = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_guild_intros(guild_id.into())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "login")?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(Redirect::to(&format!("{}/error", state.origin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_upload = true;
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
HtmxBuilder::new(Tag::Html)
|
||||||
|
.push_builder(page_header("MemeJoin - Dashboard"))
|
||||||
|
.builder(Tag::Nav, |b| {
|
||||||
|
b.builder(Tag::HeaderGroup, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Header1, |b| b.text("MemeJoin - A bot for user intros"))
|
||||||
|
.builder_text(Tag::Header6, &format!("{} - {}", user.name(), guild.name()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.builder(Tag::Empty, |b| {
|
||||||
|
// TODO:
|
||||||
|
// let mut b = if is_moderator || can_add_channel {
|
||||||
|
// b.builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder(Tag::Article, |b| {
|
||||||
|
// b.builder_text(Tag::Header, "Server Settings")
|
||||||
|
// .push_builder(mod_dashboard)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// b
|
||||||
|
// };
|
||||||
|
let b = if can_upload {
|
||||||
|
b.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
b.builder_text(Tag::Header, "Upload New Intro")
|
||||||
|
.push_builder(upload_form(&state.origin, guild_id))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
b.builder_text(Tag::Header, "Upload New Intro from Url")
|
||||||
|
.push_builder(ytdl_form(&state.origin, guild_id))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
};
|
||||||
|
|
||||||
|
b.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
let mut b = b.builder_text(Tag::Header, "Guild Intros");
|
||||||
|
|
||||||
|
for guild_channel in guild.channels() {
|
||||||
|
let intros = user.intros().get(&(guild.id(), guild_channel.name().clone())).map(|intros| intros.iter()).unwrap_or_default();
|
||||||
|
|
||||||
|
b = b.builder(Tag::Details, |b| {
|
||||||
|
let mut b = b;
|
||||||
|
if guild.channels().len() < 2 {
|
||||||
|
b = b.attribute("open", "");
|
||||||
|
}
|
||||||
|
b.builder_text(Tag::Summary, guild_channel.name().as_ref()).builder(
|
||||||
|
Tag::Div,
|
||||||
|
|b| {
|
||||||
|
b.attribute("id", "channel-intro-selector")
|
||||||
|
.attribute("style", "display: flex; align-items: flex-end; max-height: 50%; overflow: hidden;")
|
||||||
|
.push_builder(channel_intro_selector(
|
||||||
|
&state.origin,
|
||||||
|
guild_id,
|
||||||
|
guild_channel.name(),
|
||||||
|
intros,
|
||||||
|
guild_intros.iter(),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
jar: CookieJar,
|
||||||
|
) -> Result<(CookieJar, Redirect), PageError> {
|
||||||
|
let Some(code) = params.get("code") else {
|
||||||
|
return Err(ApiError::bad_request("no code").into());
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("attempting to get access token with code {}", code);
|
||||||
|
|
||||||
|
let token = state
|
||||||
|
.intro_tool_service
|
||||||
|
// TODO: decoulple discord from HTTP server
|
||||||
|
.authenticate_user::<DiscordService>(DiscordAuthParams {
|
||||||
|
origin: state.origin.clone(),
|
||||||
|
code: code.clone(),
|
||||||
|
client_id: state.secrets.client_id.clone(),
|
||||||
|
client_secret: state.secrets.client_secret.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::from)?;
|
||||||
|
let uri = Url::parse(&state.origin).expect("should be a valid url");
|
||||||
|
|
||||||
|
let mut cookie = Cookie::new("access_token", token);
|
||||||
|
cookie.set_path(uri.path().to_string());
|
||||||
|
cookie.set_secure(true);
|
||||||
|
|
||||||
|
Ok((jar.add(cookie), Redirect::to(&format!("{}/", state.origin))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_header(title: &str) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Html).head(|b| {
|
||||||
|
b.title(title)
|
||||||
|
.script(
|
||||||
|
"https://unpkg.com/htmx.org@1.9.3",
|
||||||
|
Some("sha384-lVb3Rd/Ca0AxaoZg5sACe8FJKF0tnUgR2Kd7ehUOG5GCcROv5uBIZsOqovBAcWua"),
|
||||||
|
)
|
||||||
|
// Not currently using
|
||||||
|
// .script("https://unpkg.com/hyperscript.org@0.9.9", None)
|
||||||
|
.style_link("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild_list<'a>(origin: &str, guilds: impl Iterator<Item = &'a GuildRef>) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).ul(|b| {
|
||||||
|
let mut b = b;
|
||||||
|
for guild in guilds {
|
||||||
|
b = b.li(|b| b.link(guild.name(), &format!("{}/guild/{}", origin, guild.id())));
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel_intro_selector<'a>(
|
||||||
|
origin: &str,
|
||||||
|
guild_id: u64,
|
||||||
|
channel_name: &ChannelName,
|
||||||
|
intros: impl Iterator<Item = &'a Intro>,
|
||||||
|
guild_intros: impl Iterator<Item = &'a Intro>,
|
||||||
|
) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty)
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("style", "display: flex; flex-direction: column; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 16px;")
|
||||||
|
.builder_text(Tag::Strong, "Your Current Intros")
|
||||||
|
.push_builder(intro_list(
|
||||||
|
intros,
|
||||||
|
"Remove Intro",
|
||||||
|
&format!("{}/v2/intros/remove/{}/{}", origin, guild_id, channel_name.as_ref()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("style", "display: flex; flex-direction: column; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 16px;")
|
||||||
|
.builder_text(Tag::Strong, "Select Intros")
|
||||||
|
.push_builder(intro_list(
|
||||||
|
guild_intros,
|
||||||
|
"Add Intro",
|
||||||
|
&format!("{}/v2/intros/add/{}/{}", origin, guild_id, channel_name.as_ref()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intro_list<'a>(intros: impl Iterator<Item = &'a Intro>, label: &str, post: &str) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_post(post)
|
||||||
|
.hx_target("closest #channel-intro-selector")
|
||||||
|
.attribute("hx-encoding", "multipart/form-data")
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
let mut b = b
|
||||||
|
.attribute("class", "container")
|
||||||
|
.attribute("style", "height: 256px; overflow: auto");
|
||||||
|
for intro in intros {
|
||||||
|
b = b.builder(Tag::Label, |b| {
|
||||||
|
b.builder(Tag::Input, |b| {
|
||||||
|
b.attribute("type", "checkbox")
|
||||||
|
.attribute("name", &intro.id().to_string())
|
||||||
|
})
|
||||||
|
.builder_text(Tag::Paragraph, intro.name())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text(label))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_post(&format!("{}/v2/intros/{}/upload", origin, guild_id))
|
||||||
|
.attribute("hx-encoding", "multipart/form-data")
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.attribute("role", "group")
|
||||||
|
.input(|b| b.attribute("type", "file").attribute("name", "file"))
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("name", "name")
|
||||||
|
.attribute("placeholder", "enter intro title")
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text("Upload"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ytdl_form(origin: &str, guild_id: u64) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_get(&format!("{}/v2/intros/{}/add", origin, guild_id))
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.attribute("role", "group")
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("placeholder", "enter video url")
|
||||||
|
.attribute("name", "url")
|
||||||
|
})
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("placeholder", "enter intro title")
|
||||||
|
.attribute("name", "name")
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text("Upload"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
response::{Html, IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::intro_tool::{
|
||||||
|
models::guild::{
|
||||||
|
AddIntroToGuildError, AddIntroToUserError, AddUserToGuildError, AutheticateUserError,
|
||||||
|
CreateUserError, GetChannelError, GetGuildError, GetIntroError, GetUserError,
|
||||||
|
},
|
||||||
|
ports::AuthService,
|
||||||
|
},
|
||||||
|
htmx::{Build, HtmxBuilder, Tag},
|
||||||
|
inbound::http::page::page_header,
|
||||||
|
outbound::discord::DiscordError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) trait ErrorAsRedirect<T>: Sized {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetGuildError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetGuildError::NotFound)
|
||||||
|
| Err(GetGuildError::CouldNotFetchUsers(_))
|
||||||
|
| Err(GetGuildError::CouldNotFetchChannels(_))
|
||||||
|
| Err(GetGuildError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get guild");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetChannelError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetChannelError::NotFound) | Err(GetChannelError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get channel");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetIntroError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetIntroError::NotFound) | Err(GetIntroError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get intro");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct PageError(pub ApiError);
|
||||||
|
|
||||||
|
impl IntoResponse for PageError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
Html(
|
||||||
|
page_header("MemeJoin - Error")
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder_text(
|
||||||
|
Tag::Header2,
|
||||||
|
&format!("Uh oh! - Status Code {}", self.0.status_code()),
|
||||||
|
)
|
||||||
|
.builder(Tag::Blockquote, |b| b.text(self.0.message()))
|
||||||
|
.builder(Tag::Empty, |b| b.text("<br/>"))
|
||||||
|
.builder(Tag::Anchor, |b| {
|
||||||
|
b.attribute("role", "button")
|
||||||
|
.text("Go Back")
|
||||||
|
.attribute("href", "/")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ApiResponse<T: Serialize>(StatusCode, Json<T>);
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(tag = "status")]
|
||||||
|
pub(super) enum ApiError {
|
||||||
|
NotFound {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
BadRequest {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Forbidden {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
InternalServerError {
|
||||||
|
#[serde(skip)]
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
ApiError::NotFound { .. } => StatusCode::NOT_FOUND,
|
||||||
|
ApiError::BadRequest { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
ApiError::Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||||
|
ApiError::InternalServerError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ApiError::NotFound { message } => message,
|
||||||
|
ApiError::BadRequest { message } => message,
|
||||||
|
ApiError::Forbidden { message } => message,
|
||||||
|
ApiError::InternalServerError { message } => message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn not_found(message: impl ToString) -> Self {
|
||||||
|
Self::NotFound {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bad_request(message: impl ToString) -> Self {
|
||||||
|
Self::BadRequest {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn forbidden(message: impl ToString) -> Self {
|
||||||
|
Self::Forbidden {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn internal(message: impl ToString) -> Self {
|
||||||
|
Self::InternalServerError {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
tracing::error!(err = ?self, "error");
|
||||||
|
|
||||||
|
(self.status_code(), Json(self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ApiError> for PageError {
|
||||||
|
fn from(value: ApiError) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetGuildError> for ApiError {
|
||||||
|
fn from(value: GetGuildError) -> Self {
|
||||||
|
match value {
|
||||||
|
GetGuildError::NotFound => Self::not_found("Guild not found"),
|
||||||
|
GetGuildError::CouldNotFetchUsers(get_user_error) => {
|
||||||
|
tracing::error!(err = ?get_user_error, "could not fetch users from guild");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch users from guild".to_string())
|
||||||
|
}
|
||||||
|
GetGuildError::CouldNotFetchChannels(get_channel_error) => {
|
||||||
|
tracing::error!(err = ?get_channel_error, "could not fetch channels from guild");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch channels from guild".to_string())
|
||||||
|
}
|
||||||
|
GetGuildError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetUserError> for ApiError {
|
||||||
|
fn from(value: GetUserError) -> Self {
|
||||||
|
match value {
|
||||||
|
GetUserError::NotFound => Self::not_found("User not found"),
|
||||||
|
GetUserError::CouldNotFetchGuilds(get_guild_error) => {
|
||||||
|
tracing::error!(err = ?get_guild_error, "could not fetch guilds from user");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch guilds from user".to_string())
|
||||||
|
}
|
||||||
|
GetUserError::CouldNotFetchChannelIntros(get_channel_intro_error) => {
|
||||||
|
tracing::error!(err = ?get_channel_intro_error, "could not fetch channel intros from user");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch channel intros from user".to_string())
|
||||||
|
}
|
||||||
|
GetUserError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddIntroToGuildError> for ApiError {
|
||||||
|
fn from(value: AddIntroToGuildError) -> Self {
|
||||||
|
match value {
|
||||||
|
AddIntroToGuildError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddIntroToUserError> for ApiError {
|
||||||
|
fn from(value: AddIntroToUserError) -> Self {
|
||||||
|
match value {
|
||||||
|
AddIntroToUserError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetIntroError> for ApiError {
|
||||||
|
fn from(value: GetIntroError) -> Self {
|
||||||
|
match value {
|
||||||
|
GetIntroError::NotFound => Self::not_found("Intro not found"),
|
||||||
|
GetIntroError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateUserError> for ApiError {
|
||||||
|
fn from(value: CreateUserError) -> Self {
|
||||||
|
match value {
|
||||||
|
CreateUserError::CouldNotGetUser(err) => err.into(),
|
||||||
|
CreateUserError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddUserToGuildError> for ApiError {
|
||||||
|
fn from(value: AddUserToGuildError) -> Self {
|
||||||
|
match value {
|
||||||
|
AddUserToGuildError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: AuthService> From<AutheticateUserError<A>> for ApiError
|
||||||
|
where
|
||||||
|
<A as AuthService>::Error: Into<ApiError>,
|
||||||
|
{
|
||||||
|
fn from(value: AutheticateUserError<A>) -> Self {
|
||||||
|
match value {
|
||||||
|
AutheticateUserError::CouldNotFetchGuild(err) => err.into(),
|
||||||
|
AutheticateUserError::CouldNotCreateUser(err) => err.into(),
|
||||||
|
AutheticateUserError::CouldNotFetchUser(err) => err.into(),
|
||||||
|
AutheticateUserError::CouldNotAddUserToGuild(err) => err.into(),
|
||||||
|
AutheticateUserError::UserNotPartOfInstanceGuilds => {
|
||||||
|
Self::internal("User not part of instance guilds")
|
||||||
|
}
|
||||||
|
AutheticateUserError::ExternalError(err) => err.into(),
|
||||||
|
AutheticateUserError::Unknown(err) => {
|
||||||
|
tracing::error!(err = ?err, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DiscordError> for ApiError {
|
||||||
|
fn from(value: DiscordError) -> Self {
|
||||||
|
match value {
|
||||||
|
DiscordError::ApiRequest(error) => {
|
||||||
|
tracing::error!(err = ?error, "api request error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for ApiError {
|
||||||
|
fn from(value: reqwest::Error) -> Self {
|
||||||
|
Self::internal(format!("error making request to external service: {value}",))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for ApiError {
|
||||||
|
fn from(value: anyhow::Error) -> Self {
|
||||||
|
Self::internal(format!("unknown error: {value}",))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod htmx;
|
||||||
|
pub mod inbound;
|
||||||
|
pub mod outbound;
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::{
|
||||||
|
models::guild::{ExternalGuildId, UserName},
|
||||||
|
ports::{AuthService, ExternalUser},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DiscordService;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DiscordUser {
|
||||||
|
token: String,
|
||||||
|
username: String,
|
||||||
|
guilds: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DiscordError {
|
||||||
|
#[error(transparent)]
|
||||||
|
ApiRequest(#[from] reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiscordAuthParams {
|
||||||
|
pub origin: String,
|
||||||
|
pub code: String,
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DiscordApiAuth {
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
expires_in: usize,
|
||||||
|
refresh_token: String,
|
||||||
|
scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DiscordApiUser {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DiscordUserGuild {
|
||||||
|
#[serde(deserialize_with = "serde_string_as_u64")]
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
owner: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthService for DiscordService {
|
||||||
|
type Params = DiscordAuthParams;
|
||||||
|
type User = DiscordUser;
|
||||||
|
type Error = DiscordError;
|
||||||
|
|
||||||
|
async fn authenticate_user(params: Self::Params) -> Result<Self::User, Self::Error> {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
|
||||||
|
let redirect_uri = format!("{}/v2/auth", params.origin);
|
||||||
|
data.insert("client_id", params.client_id.as_str());
|
||||||
|
data.insert("client_secret", params.client_secret.as_str());
|
||||||
|
data.insert("grant_type", "authorization_code");
|
||||||
|
data.insert("code", ¶ms.code);
|
||||||
|
data.insert("redirect_uri", &redirect_uri);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let auth: DiscordApiAuth = client
|
||||||
|
.post("https://discord.com/api/oauth2/token")
|
||||||
|
.form(&data)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get authorized username
|
||||||
|
let user: DiscordApiUser = client
|
||||||
|
.get("https://discord.com/api/v10/users/@me")
|
||||||
|
.bearer_auth(&auth.access_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: get bot's guilds so we only save users who are able to use the bot
|
||||||
|
let discord_guilds: Vec<DiscordUserGuild> = client
|
||||||
|
.get("https://discord.com/api/v10/users/@me/guilds")
|
||||||
|
.bearer_auth(&auth.access_token)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self::User {
|
||||||
|
token: auth.access_token,
|
||||||
|
username: user.username,
|
||||||
|
guilds: discord_guilds.into_iter().map(|guild| guild.id).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExternalUser for DiscordUser {
|
||||||
|
fn external_token(&self) -> &str {
|
||||||
|
&self.token
|
||||||
|
}
|
||||||
|
|
||||||
|
fn username(&self) -> UserName {
|
||||||
|
self.username.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guilds(&self) -> impl Iterator<Item = ExternalGuildId> {
|
||||||
|
self.guilds.iter().map(|id| ExternalGuildId(*id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serde_string_as_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = <&str as Deserialize>::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
value
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &"u64"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::ports::LocalAudioFetcher;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Ffmpeg;
|
||||||
|
|
||||||
|
impl LocalAudioFetcher for Ffmpeg {
|
||||||
|
async fn save_local_audio(&self, bytes: &[u8], name: &str) -> Result<String, anyhow::Error> {
|
||||||
|
let temp_path = format!("./sounds/temp/{name}");
|
||||||
|
let dest_path = format!("./sounds/{name}.mp3");
|
||||||
|
|
||||||
|
// Write original file so its ready for codec conversion
|
||||||
|
std::fs::write(&temp_path, bytes).context("failed to write temp file")?;
|
||||||
|
let child = tokio::process::Command::new("ffmpeg")
|
||||||
|
.args(["-i", &temp_path])
|
||||||
|
.arg("-vn")
|
||||||
|
.args(["-map", "0:a"])
|
||||||
|
.arg(&dest_path)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|err| anyhow!(err.to_string()))?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|err| anyhow!(err.to_string()))?;
|
||||||
|
|
||||||
|
if !child.success() {
|
||||||
|
return Err(anyhow!("ffmpeg terminated unsuccessfully"));
|
||||||
|
}
|
||||||
|
std::fs::remove_file(&temp_path).context("failed to remove temp file")?;
|
||||||
|
|
||||||
|
Ok(dest_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod discord;
|
||||||
|
pub mod ffmpeg;
|
||||||
|
pub mod sqlite;
|
||||||
|
pub mod ytdlp;
|
||||||
|
|
@ -0,0 +1,538 @@
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use iter_tools::Itertools;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::{
|
||||||
|
models::guild::{
|
||||||
|
self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel,
|
||||||
|
ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError,
|
||||||
|
CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError,
|
||||||
|
GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, IntroId, User, UserName,
|
||||||
|
},
|
||||||
|
ports::IntroToolRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Sqlite {
|
||||||
|
conn: Arc<Mutex<Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sqlite {
|
||||||
|
pub fn new(path: &str) -> rusqlite::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
conn: Arc::new(Mutex::new(Connection::open(path)?)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntroToolRepository for Sqlite {
|
||||||
|
async fn get_guild(&self, guild_id: GuildId) -> Result<Guild, GetGuildError> {
|
||||||
|
let guild = {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
select
|
||||||
|
Guild.id,
|
||||||
|
Guild.name,
|
||||||
|
Guild.sound_delay
|
||||||
|
from Guild
|
||||||
|
where Guild.id = :guild_id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
query
|
||||||
|
.query_row(&[(":guild_id", &guild_id.to_string())], |row| {
|
||||||
|
Ok(Guild::new(
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to query row")?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(guild
|
||||||
|
.with_users(self.get_guild_users(guild_id).await?)
|
||||||
|
.with_channels(self.get_guild_channels(guild_id).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guilds(&self) -> Result<Vec<GuildRef>, GetGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
Guild.id,
|
||||||
|
Guild.name,
|
||||||
|
Guild.sound_delay
|
||||||
|
FROM Guild
|
||||||
|
LEFT JOIN UserGuild ON Guild.id = UserGuild.guild_id
|
||||||
|
LEFT JOIN User ON User.username = UserGuild.username
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let guilds = query
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok(GuildRef::new(
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild user rows")?;
|
||||||
|
|
||||||
|
Ok(guilds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_count(&self) -> Result<usize, GetGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
select
|
||||||
|
count(*)
|
||||||
|
from Guild
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
Ok(query
|
||||||
|
.query_row([], |row| row.get::<_, usize>(0))
|
||||||
|
.context("failed to query row")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_users(&self, guild_id: GuildId) -> Result<Vec<User>, GetUserError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
User.username AS name,
|
||||||
|
User.api_key,
|
||||||
|
User.api_key_expires_at,
|
||||||
|
User.discord_token,
|
||||||
|
User.discord_token_expires_at
|
||||||
|
FROM UserGuild
|
||||||
|
LEFT JOIN User ON User.username = UserGuild.username
|
||||||
|
WHERE UserGuild.guild_id = :guild_id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let users = query
|
||||||
|
.query_map(&[(":guild_id", &guild_id.to_string())], |row| {
|
||||||
|
Ok(User::new(
|
||||||
|
UserName::from(row.get::<_, String>(0)?),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
row.get(3)?,
|
||||||
|
row.get(4)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild user rows")?;
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_channels(&self, guild_id: GuildId) -> Result<Vec<Channel>, GetChannelError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
Channel.name
|
||||||
|
FROM Channel
|
||||||
|
WHERE
|
||||||
|
Channel.guild_id = :guild_id
|
||||||
|
ORDER BY Channel.name DESC
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let channels = query
|
||||||
|
.query_map(&[(":guild_id", &guild_id.to_string())], |row| {
|
||||||
|
Ok(Channel::new(row.get::<_, String>(0)?.into()))
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild channel rows")?;
|
||||||
|
|
||||||
|
Ok(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_intros(&self, guild_id: GuildId) -> Result<Vec<Intro>, GetIntroError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
Intro.id,
|
||||||
|
Intro.name,
|
||||||
|
Intro.filename
|
||||||
|
FROM Intro
|
||||||
|
WHERE
|
||||||
|
Intro.guild_id = :guild_id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let intros = query
|
||||||
|
.query_map(&[(":guild_id", &guild_id.to_string())], |row| {
|
||||||
|
Ok(Intro::new(
|
||||||
|
row.get::<_, i32>(0)?.into(),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild intro rows")?;
|
||||||
|
|
||||||
|
Ok(intros)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(&self, username: impl AsRef<str>) -> Result<User, GetUserError> {
|
||||||
|
let user = {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
username AS name, api_key, api_key_expires_at, discord_token, discord_token_expires_at
|
||||||
|
FROM User
|
||||||
|
WHERE username = :username
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
query
|
||||||
|
.query_row(&[(":username", username.as_ref())], |row| {
|
||||||
|
Ok(User::new(
|
||||||
|
UserName::from(row.get::<_, String>(0)?),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
row.get(3)?,
|
||||||
|
row.get(4)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to query row")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let guilds = self
|
||||||
|
.get_user_guilds(username.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(Box::new)?;
|
||||||
|
|
||||||
|
let mut intros = HashMap::new();
|
||||||
|
for guild in guilds {
|
||||||
|
intros.extend(
|
||||||
|
self.get_user_channel_intros(username.as_ref(), guild.id())
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user.with_channel_intros(intros))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_channel_intros(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str>,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<HashMap<(GuildId, ChannelName), Vec<Intro>>, GetIntroError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
struct ChannelIntro {
|
||||||
|
channel_name: ChannelName,
|
||||||
|
intro: Intro,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
Intro.id,
|
||||||
|
Intro.name,
|
||||||
|
Intro.filename,
|
||||||
|
UI.channel_name
|
||||||
|
FROM Intro
|
||||||
|
LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id
|
||||||
|
WHERE
|
||||||
|
UI.username = ?1
|
||||||
|
AND UI.guild_id = ?2
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let intros = query
|
||||||
|
.query_map([username.as_ref(), &guild_id.to_string()], |row| {
|
||||||
|
Ok(ChannelIntro {
|
||||||
|
channel_name: ChannelName::from(row.get::<_, String>(3)?),
|
||||||
|
intro: Intro::new(row.get::<_, i32>(0)?.into(), row.get(1)?, row.get(2)?),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<Vec<ChannelIntro>, _>>()
|
||||||
|
.context("failed to fetch user channel intro rows")?;
|
||||||
|
|
||||||
|
let intros = intros
|
||||||
|
.into_iter()
|
||||||
|
.map(|intro| ((guild_id, intro.channel_name), intro.intro))
|
||||||
|
.into_group_map();
|
||||||
|
|
||||||
|
Ok(intros)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str>,
|
||||||
|
) -> Result<Vec<GuildRef>, GetGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
Guild.id,
|
||||||
|
Guild.name,
|
||||||
|
Guild.sound_delay
|
||||||
|
FROM Guild
|
||||||
|
LEFT JOIN UserGuild ON Guild.id = UserGuild.guild_id
|
||||||
|
LEFT JOIN User ON User.username = UserGuild.username
|
||||||
|
WHERE User.username = :username
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let guilds = query
|
||||||
|
.query_map(&[(":username", username.as_ref())], |row| {
|
||||||
|
Ok(GuildRef::new(
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
row.get(1)?,
|
||||||
|
row.get(2)?,
|
||||||
|
row.get::<_, u64>(0)?.into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.context("failed to map prepared query")?
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild user rows")?;
|
||||||
|
|
||||||
|
Ok(guilds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_api_key(&self, api_key: &str) -> Result<User, GetUserError> {
|
||||||
|
let username = {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
username AS name
|
||||||
|
FROM User
|
||||||
|
WHERE api_key = :api_key
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
query
|
||||||
|
.query_row(&[(":api_key", api_key)], |row| {
|
||||||
|
Ok(UserName::from(row.get::<_, String>(0)?))
|
||||||
|
})
|
||||||
|
.context("failed to query row")?
|
||||||
|
};
|
||||||
|
|
||||||
|
self.get_user(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_user_api_key(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
api_key: &str,
|
||||||
|
expires_at: NaiveDateTime,
|
||||||
|
) -> Result<(), GetUserError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
UPDATE User
|
||||||
|
SET api_key = ?1, api_key_expires_at = ?2
|
||||||
|
WHERE username = ?3
|
||||||
|
",
|
||||||
|
[api_key, &expires_at.to_string(), username],
|
||||||
|
)
|
||||||
|
.context("failed to update user api key")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_user_intro(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToUserRequest,
|
||||||
|
) -> Result<(), guild::AddIntroToUserError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
DELETE FROM UserIntro
|
||||||
|
WHERE username = ?1
|
||||||
|
AND guild_id = ?2
|
||||||
|
AND channel_name = ?3
|
||||||
|
",
|
||||||
|
[
|
||||||
|
&req.user.to_string(),
|
||||||
|
&req.guild_id.to_string(),
|
||||||
|
&req.channel_name.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.context("failed to delete user intros")?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT INTO
|
||||||
|
UserIntro (username, guild_id, channel_name, intro_id)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
[
|
||||||
|
&req.user.to_string(),
|
||||||
|
&req.guild_id.to_string(),
|
||||||
|
&req.channel_name.to_string(),
|
||||||
|
&req.intro_id.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.context("failed to insert user intro")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let guild_id: GuildId = req.external_id.0.into();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT INTO
|
||||||
|
Guild (id, name, sound_delay)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
",
|
||||||
|
[
|
||||||
|
&guild_id.to_string(),
|
||||||
|
&req.name,
|
||||||
|
&req.sound_delay.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.context("failed to insert guild")?;
|
||||||
|
|
||||||
|
Ok(Guild::new(
|
||||||
|
guild_id,
|
||||||
|
req.name,
|
||||||
|
req.sound_delay,
|
||||||
|
req.external_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(&self, req: CreateUserRequest) -> Result<(), CreateUserError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT INTO
|
||||||
|
User (username)
|
||||||
|
VALUES (?1)
|
||||||
|
",
|
||||||
|
[req.user.as_ref()],
|
||||||
|
)
|
||||||
|
.context("failed to insert user")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user_to_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<(), guild::AddUserToGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT OR IGNORE INTO UserGuild (username, guild_id) VALUES (?1, ?2)
|
||||||
|
",
|
||||||
|
[username, &guild_id.to_string()],
|
||||||
|
)
|
||||||
|
.context("failed to insert user guild")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: CreateChannelRequest,
|
||||||
|
) -> Result<Channel, CreateChannelError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
guild_id: GuildId,
|
||||||
|
filename: String,
|
||||||
|
) -> Result<IntroId, AddIntroToGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
INSERT INTO Intro
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
volume,
|
||||||
|
guild_id,
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:name,
|
||||||
|
:volume,
|
||||||
|
:guild_id,
|
||||||
|
:filename
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let intro_id = query
|
||||||
|
.query_row(
|
||||||
|
&[
|
||||||
|
(":name", name),
|
||||||
|
(":volume", &0.to_string()),
|
||||||
|
(":guild_id", &guild_id.to_string()),
|
||||||
|
(":filename", &filename),
|
||||||
|
],
|
||||||
|
|row| Ok(row.get::<_, i32>(0)?.into()),
|
||||||
|
)
|
||||||
|
.context("failed to query row")?;
|
||||||
|
|
||||||
|
Ok(intro_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
|
use crate::domain::intro_tool::ports::RemoteAudioFetcher;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Ytdlp;
|
||||||
|
|
||||||
|
impl RemoteAudioFetcher for Ytdlp {
|
||||||
|
async fn fetch_remote_audio(&self, url: &str, name: &str) -> Result<String, anyhow::Error> {
|
||||||
|
let file_name = format!("sounds/{name}");
|
||||||
|
|
||||||
|
let child = tokio::process::Command::new("yt-dlp")
|
||||||
|
.arg(url)
|
||||||
|
.args(["-o", &file_name])
|
||||||
|
.args(["-x", "--audio-format", "mp3"])
|
||||||
|
.spawn()
|
||||||
|
.context("failed to spawn yt-dlp process")?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.context("yt-dlp process failed")?;
|
||||||
|
|
||||||
|
if !child.success() {
|
||||||
|
return Err(anyhow!("yt-dlp terminated unsuccessfully"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("{file_name}.mp3"))
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/main.rs
168
src/main.rs
|
|
@ -1,24 +1,9 @@
|
||||||
// #![feature(stmt_expr_attributes)]
|
|
||||||
// #![feature(proc_macro_hygiene)]
|
|
||||||
// #![feature(async_closure)]
|
|
||||||
|
|
||||||
mod auth;
|
|
||||||
mod db;
|
mod db;
|
||||||
mod htmx;
|
|
||||||
mod media;
|
|
||||||
mod page;
|
|
||||||
mod routes;
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
use axum::http::Method;
|
|
||||||
use axum::routing::{get, post};
|
|
||||||
use axum::Router;
|
|
||||||
use settings::ApiState;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
|
||||||
|
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::model::prelude::{Channel, ChannelId, GuildId, Member, Ready};
|
use serenity::model::prelude::{Channel, ChannelId, GuildId, Member, Ready};
|
||||||
|
|
@ -28,7 +13,7 @@ use serenity::prelude::*;
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use crate::settings::Settings;
|
use memejoin_rs::{auth, domain::intro_tool, inbound, outbound};
|
||||||
|
|
||||||
enum HandlerMessage {
|
enum HandlerMessage {
|
||||||
Ready(Context),
|
Ready(Context),
|
||||||
|
|
@ -116,67 +101,6 @@ impl EventHandler for Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_api(db: Arc<tokio::sync::Mutex<db::Database>>) {
|
|
||||||
let secrets = auth::DiscordSecret {
|
|
||||||
client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"),
|
|
||||||
client_secret: env::var("DISCORD_CLIENT_SECRET")
|
|
||||||
.expect("expected DISCORD_CLIENT_SECRET env var"),
|
|
||||||
bot_token: env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"),
|
|
||||||
};
|
|
||||||
let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN");
|
|
||||||
|
|
||||||
let state = ApiState {
|
|
||||||
db,
|
|
||||||
secrets,
|
|
||||||
origin: origin.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let api = Router::new()
|
|
||||||
.route("/", get(page::home))
|
|
||||||
.route("/index.html", get(page::home))
|
|
||||||
.route("/login", get(page::login))
|
|
||||||
.route("/guild/:guild_id", get(page::guild_dashboard))
|
|
||||||
.route("/guild/:guild_id/setup", get(routes::guild_setup))
|
|
||||||
.route(
|
|
||||||
"/guild/:guild_id/add_channel",
|
|
||||||
post(routes::guild_add_channel),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/guild/:guild_id/permissions/update",
|
|
||||||
post(routes::update_guild_permissions),
|
|
||||||
)
|
|
||||||
.route("/v2/auth", get(routes::v2_auth))
|
|
||||||
.route(
|
|
||||||
"/v2/intros/add/:guild_id/:channel",
|
|
||||||
post(routes::v2_add_intro_to_user),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v2/intros/remove/:guild_id/:channel",
|
|
||||||
post(routes::v2_remove_intro_from_user),
|
|
||||||
)
|
|
||||||
.route("/v2/intros/:guild/add", get(routes::v2_add_guild_intro))
|
|
||||||
.route(
|
|
||||||
"/v2/intros/:guild/upload",
|
|
||||||
post(routes::v2_upload_guild_intro),
|
|
||||||
)
|
|
||||||
.route("/health", get(routes::health))
|
|
||||||
.layer(
|
|
||||||
CorsLayer::new()
|
|
||||||
.allow_origin([origin.parse().unwrap()])
|
|
||||||
.allow_headers(Any)
|
|
||||||
.allow_methods([Method::GET, Method::POST, Method::DELETE]),
|
|
||||||
)
|
|
||||||
.with_state(state);
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8100));
|
|
||||||
info!("socket listening on {addr}");
|
|
||||||
axum::Server::bind(&addr)
|
|
||||||
.serve(api.into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn spawn_bot(db: Arc<tokio::sync::Mutex<db::Database>>) {
|
async fn spawn_bot(db: Arc<tokio::sync::Mutex<db::Database>>) {
|
||||||
let token = env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var");
|
let token = env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var");
|
||||||
let songbird = songbird::Songbird::serenity();
|
let songbird = songbird::Songbird::serenity();
|
||||||
|
|
@ -312,37 +236,75 @@ async fn spawn_bot(db: Arc<tokio::sync::Mutex<db::Database>>) {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let settings = serde_json::from_str::<Settings>(
|
tracing::info!("tracing initialized");
|
||||||
&std::fs::read_to_string("config/settings.json").expect("no config/settings.json"),
|
|
||||||
)
|
|
||||||
.expect("error parsing settings file");
|
|
||||||
info!("{settings:?}");
|
|
||||||
|
|
||||||
let (run_api, run_bot) = (settings.run_api, settings.run_bot);
|
let secrets = auth::DiscordSecret {
|
||||||
let db = Arc::new(tokio::sync::Mutex::new(
|
client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"),
|
||||||
db::Database::new("./config/db.sqlite").expect("couldn't open sqlite db"),
|
client_secret: env::var("DISCORD_CLIENT_SECRET")
|
||||||
));
|
.expect("expected DISCORD_CLIENT_SECRET env var"),
|
||||||
|
bot_token: env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"),
|
||||||
|
};
|
||||||
|
let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN");
|
||||||
|
|
||||||
{
|
let db = outbound::sqlite::Sqlite::new("./config/db.sqlite").expect("couldn't open sqlite db");
|
||||||
// attempt to initialize the database with the schema
|
let local_audio_fetcher = outbound::ffmpeg::Ffmpeg;
|
||||||
let db = db.lock().await;
|
let remote_audio_fetcher = outbound::ytdlp::Ytdlp;
|
||||||
db.init().expect("couldn't init db");
|
|
||||||
|
if let Ok(impersonated_username) = env::var("IMPERSONATED_USERNAME") {
|
||||||
|
let service =
|
||||||
|
intro_tool::service::Service::new(db, remote_audio_fetcher, local_audio_fetcher);
|
||||||
|
let service = intro_tool::debug_service::DebugService::new(service, impersonated_username);
|
||||||
|
|
||||||
|
let http_server = inbound::http::HttpServer::new(service, secrets, origin)
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
|
||||||
|
http_server.run().await;
|
||||||
|
} else {
|
||||||
|
let service =
|
||||||
|
intro_tool::service::Service::new(db, remote_audio_fetcher, local_audio_fetcher);
|
||||||
|
|
||||||
|
let http_server = inbound::http::HttpServer::new(service, secrets, origin)
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
|
||||||
|
http_server.run().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if run_api {
|
|
||||||
spawn_api(db.clone());
|
|
||||||
}
|
|
||||||
if run_bot {
|
|
||||||
spawn_bot(db).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("spawned background tasks");
|
|
||||||
|
|
||||||
let _ = tokio::signal::ctrl_c().await;
|
|
||||||
info!("Received Ctrl-C, shuttdown down.");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
|
// dotenv::dotenv().ok();
|
||||||
|
//
|
||||||
|
// tracing_subscriber::fmt::init();
|
||||||
|
//
|
||||||
|
// let settings = serde_json::from_str::<Settings>(
|
||||||
|
// &std::fs::read_to_string("config/settings.json").expect("no config/settings.json"),
|
||||||
|
// )
|
||||||
|
// .expect("error parsing settings file");
|
||||||
|
// info!("{settings:?}");
|
||||||
|
//
|
||||||
|
// let (run_api, run_bot) = (settings.run_api, settings.run_bot);
|
||||||
|
// let db = Arc::new(tokio::sync::Mutex::new(
|
||||||
|
// db::Database::new("./config/db.sqlite").expect("couldn't open sqlite db"),
|
||||||
|
// ));
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// // attempt to initialize the database with the schema
|
||||||
|
// let db = db.lock().await;
|
||||||
|
// db.init().expect("couldn't init db");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if run_api {
|
||||||
|
// spawn_api(db.clone());
|
||||||
|
// }
|
||||||
|
// if run_bot {
|
||||||
|
// spawn_bot(db).await;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// info!("spawned background tasks");
|
||||||
|
//
|
||||||
|
// let _ = tokio::signal::ctrl_c().await;
|
||||||
|
// info!("Received Ctrl-C, shuttdown down.");
|
||||||
|
//
|
||||||
|
// Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue