Merge pull request 'dms are cool' (#1) from dev into main
Reviewed-on: https://g.tylerm.dev/tylerm/xssbook/pulls/1
This commit is contained in:
commit
edbbdf72c7
24 changed files with 1737 additions and 315 deletions
362
Cargo.lock
generated
362
Cargo.lock
generated
|
@ -21,13 +21,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.63"
|
||||
version = "0.1.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1"
|
||||
checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -38,12 +38,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.4"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc"
|
||||
checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64 0.21.2",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
|
@ -62,19 +63,20 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.3.2"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34"
|
||||
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
@ -93,6 +95,12 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.1"
|
||||
|
@ -245,17 +253,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.4.0"
|
||||
name = "data-encoding"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
|
@ -338,31 +339,6 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.26"
|
||||
|
@ -370,74 +346,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.26"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.26"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.26"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.26"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.26"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
|
@ -462,7 +411,7 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
@ -476,24 +425,6 @@ dependencies = [
|
|||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"no-std-compat",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"quanta",
|
||||
"rand",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.2.1"
|
||||
|
@ -527,7 +458,7 @@ version = "0.3.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
|
@ -557,9 +488,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
|
@ -597,9 +528,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.23"
|
||||
version = "0.14.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
|
||||
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -618,6 +549,16 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.5"
|
||||
|
@ -709,15 +650,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.0"
|
||||
|
@ -772,7 +704,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
|
@ -785,24 +717,6 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
|
@ -911,7 +825,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -952,34 +866,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.50"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
|
||||
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"mach",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.23"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -1014,15 +912,6 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "10.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.6.1"
|
||||
|
@ -1109,7 +998,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1218,6 +1107,17 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.1"
|
||||
|
@ -1226,22 +1126,22 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.38"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1300,6 +1200,21 @@ dependencies = [
|
|||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.25.0"
|
||||
|
@ -1328,7 +1243,19 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1397,7 +1324,6 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
@ -1414,26 +1340,6 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tower_governor"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6be418f6d18863291f0a7fa1da1de71495a19a54b5fb44969136f731a47e86"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"governor",
|
||||
"http",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.37"
|
||||
|
@ -1455,7 +1361,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1499,6 +1405,25 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
|
@ -1514,12 +1439,44 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
@ -1548,12 +1505,6 @@ dependencies = [
|
|||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -1581,7 +1532,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -1603,7 +1554,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.107",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -1614,16 +1565,6 @@ version = "0.2.83"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
|
@ -1726,7 +1667,6 @@ dependencies = [
|
|||
"tower",
|
||||
"tower-cookies",
|
||||
"tower-http",
|
||||
"tower_governor",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
|
|
@ -5,9 +5,8 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
axum = { version = "0.6.4", features = ["headers", "query"] }
|
||||
axum = { version = "0.6.12", features = ["headers", "query", "ws"] }
|
||||
tower-http = { version = "0.3.5", features = ["fs"] }
|
||||
tower_governor = "0.0.4"
|
||||
tower-cookies = "0.8.0"
|
||||
tower = "0.4.13"
|
||||
tracing = "0.1.37"
|
||||
|
@ -19,4 +18,4 @@ rusqlite = { version = "0.28.0", features = ["bundled"] }
|
|||
rand = "0.8.5"
|
||||
time = "0.3.17"
|
||||
lazy_static = "1.4"
|
||||
image = "0.24.5"
|
||||
image = "0.24.5"
|
||||
|
|
28
public/chat.html
Normal file
28
public/chat.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>XSSBook - Home</title>
|
||||
|
||||
<meta name="author" content="Tyler Murphy">
|
||||
<meta name="description" content="Home">
|
||||
|
||||
<meta property="og:title" content="xssbook">
|
||||
<meta property="og:site_name" content="xssbook.com">
|
||||
<meta property="og:description" content="Home">
|
||||
|
||||
<meta itemprop="name" content="xssbook">
|
||||
<meta itemprop="description" content="Home">
|
||||
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/header.css">
|
||||
<link rel="stylesheet" href="/css/people.css">
|
||||
<link rel="stylesheet" href="/css/chat.css">
|
||||
|
||||
<script type="module" src="/js/chat.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -123,6 +123,14 @@ h2 {
|
|||
background-color: #bfa354;
|
||||
}
|
||||
|
||||
.get {
|
||||
background-color: #00cc00;
|
||||
}
|
||||
|
||||
.delete {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
.key {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
|
|
180
public/css/chat.css
Normal file
180
public/css/chat.css
Normal file
|
@ -0,0 +1,180 @@
|
|||
.spacer {
|
||||
margin-bottom: 3.5em !important;
|
||||
}
|
||||
|
||||
#cent {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100vh - 3.5em);
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
min-width: 25em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#sidebar>span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
#center {
|
||||
height: 100%;
|
||||
width: calc(100vw - 20%);
|
||||
max-width: calc(100vw - 25em);
|
||||
}
|
||||
|
||||
.room {
|
||||
width: calc (100% - 1rem);
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.room:hover, .current {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
.room-icon {
|
||||
border-radius: 3rem;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 1000;
|
||||
font-size: 1.5rem;
|
||||
|
||||
right: 0;}
|
||||
|
||||
.room-name {
|
||||
display: flex;
|
||||
height: 3rem;
|
||||
margin-left: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.roomDisplay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.roomDisplayCenter {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: calc(100% - 25em);
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.roomDisplayPeople {
|
||||
width: 25em;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.roomDisplayPeople>span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin: .5em 0em;
|
||||
}
|
||||
|
||||
.person {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.person img, .person .profile {
|
||||
height: 7em;
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
.person .info {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.person .ltext {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.person .gtext {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.roomDisplay .messages {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.roomDisplay .messageContent {
|
||||
flex-grow: 0;
|
||||
width: auto;
|
||||
margin-bottom: .5rem;
|
||||
display: block;
|
||||
height: fit-content;
|
||||
overflow: none;
|
||||
}
|
||||
|
||||
.roomDisplay .messageContent[contenteditable]:empty::before {
|
||||
content: "Send an unencrypted message";
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.addUser[contenteditable]:empty::before {
|
||||
content: "Type email to add user";
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.addRoom[contenteditable]:empty::before {
|
||||
content: "Type name to create room";
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: fit-content;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.message-pfp {
|
||||
flex-grow: 0;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.message-pfp img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.loadMessages {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -184,4 +184,4 @@ body {
|
|||
width: calc(100% - 20px);
|
||||
background-color: var(--secondary);
|
||||
font-family: sfpro;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ body {
|
|||
--popup: #ffffffcc;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary);
|
||||
width: 100%;
|
||||
|
@ -130,7 +134,7 @@ footer {
|
|||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
input {
|
||||
input, .input {
|
||||
flex: 1;
|
||||
font-family: sfpro;
|
||||
background-color: var(--primary);
|
||||
|
@ -431,4 +435,4 @@ form {
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -141,4 +141,28 @@ export const updateavatar = async (file) => {
|
|||
|
||||
export const updatebanner = async (file) => {
|
||||
return await fileRequest('/users/banner', file, 'PUT')
|
||||
}
|
||||
}
|
||||
|
||||
export const chatlist = async () => {
|
||||
return await request('/chat/list', {})
|
||||
}
|
||||
|
||||
export const chatcreate = async (name) => {
|
||||
return await request('/chat/create', {name})
|
||||
}
|
||||
|
||||
export const chatadd = async (email, room_id) => {
|
||||
return await request('/chat/add', {email, room_id}, 'PATCH')
|
||||
}
|
||||
|
||||
export const chatleave = async (room_id) => {
|
||||
return await request('/chat/leave', {room_id}, 'DELETE')
|
||||
}
|
||||
|
||||
export const chatsend = async (content, room_id) => {
|
||||
return await request('/chat/send', {content, room_id})
|
||||
}
|
||||
|
||||
export const chatload = async (newest_msg, page, room_id) => {
|
||||
return await request('/chat/load', {newest_msg, page, room_id})
|
||||
}
|
||||
|
|
240
public/js/chat.js
Normal file
240
public/js/chat.js
Normal file
|
@ -0,0 +1,240 @@
|
|||
import { body, div, span, p, parse } from './main.js'
|
||||
import { loadself, chatlist, chatload, loadusers, chatcreate } from './api.js'
|
||||
import { createRoomDisplay, header, parseMessage, parseRoom, parseUser, createSingleLineInput } from './components.js'
|
||||
|
||||
async function getUser(user_id) {
|
||||
if (data.users[user_id]) {
|
||||
return data.users[user_id]
|
||||
} else {
|
||||
let request = (await loadusers([user_id]))
|
||||
if (request.status != 200) {
|
||||
location.href = 'login'
|
||||
} else {
|
||||
data.users[user_id] = request.json[0]
|
||||
return request.json[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function parseMessageImpl(message) {
|
||||
let user = await getUser(message.user_id)
|
||||
return parseMessage(message, user)
|
||||
}
|
||||
|
||||
async function onRoomClick(room) {
|
||||
for (const room of Object.values(data.rooms)) {
|
||||
room.display.style.display = 'none'
|
||||
room.button.classList.remove('current')
|
||||
}
|
||||
room.display.style.display = ''
|
||||
room.button.classList.add('current')
|
||||
}
|
||||
|
||||
async function render() {
|
||||
let new_body =
|
||||
body({},
|
||||
...header(false, false, true, data.self.user_id),
|
||||
div({id: 'cent'},
|
||||
div({id: 'sidebar'},
|
||||
span({class: 'ltext'},
|
||||
parse("Rooms")
|
||||
),
|
||||
createSingleLineInput(
|
||||
{
|
||||
type: 'text',
|
||||
name: 'addRoom',
|
||||
class: 'addRoom input',
|
||||
style: 'flex-grow: 0; width: 80%; margin-left: auto; margin-right: auto;'
|
||||
},
|
||||
async (text) => {
|
||||
let result = (await chatcreate(text))
|
||||
if (result.status != 201) {
|
||||
alert(result.msg)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
div({id: 'center'})
|
||||
)
|
||||
)
|
||||
|
||||
document.body.replaceWith(new_body)
|
||||
}
|
||||
|
||||
const data = {
|
||||
self: {},
|
||||
users: [],
|
||||
rooms: {},
|
||||
}
|
||||
|
||||
async function loadRoomPage(room) {
|
||||
|
||||
let request = (await chatload (
|
||||
room.newest_msg,
|
||||
room.page,
|
||||
room.room_id
|
||||
))
|
||||
|
||||
if (request.json == undefined) {
|
||||
alert(request.msg)
|
||||
return
|
||||
}
|
||||
|
||||
let messages = room.display.getElementsByClassName('messages')[0]
|
||||
for (const msg of request.json) {
|
||||
room.messages.push(msg)
|
||||
messages.appendChild(await parseMessageImpl(msg))
|
||||
}
|
||||
|
||||
room.page++
|
||||
|
||||
return request.json.length > 0
|
||||
}
|
||||
|
||||
async function loadRoom(room_id) {
|
||||
let room = data.rooms[room_id]
|
||||
|
||||
let batch = []
|
||||
for (const user_id of room.users) {
|
||||
if (data.users[user_id]) continue
|
||||
batch.push(user_id)
|
||||
}
|
||||
|
||||
if (batch.length > 1) {
|
||||
let request = (await loadusers(batch))
|
||||
if (request.status != 200) {
|
||||
location.href = '/login'
|
||||
} else {
|
||||
for (const user of request.json) {
|
||||
data.users[user.user_id] = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
room.display = createRoomDisplay(room, loadRoomPage)
|
||||
|
||||
let displays = document.getElementById("center")
|
||||
displays.appendChild(room.display)
|
||||
|
||||
let button = parseRoom(room, onRoomClick)
|
||||
|
||||
if (displays.children.length > 1) {
|
||||
room.display.style.display = 'none'
|
||||
} else {
|
||||
button.classList.add('current')
|
||||
}
|
||||
|
||||
room.page = 0
|
||||
room.messages = []
|
||||
if (room.newest_msg == undefined || room.newest_msg < 0)
|
||||
room.newest_msg = Number.MAX_SAFE_INTEGER
|
||||
await loadRoomPage(room)
|
||||
room.newest_msg = Math.min(
|
||||
...room.messages.map(m => m.message_id)
|
||||
)
|
||||
room.page = 0
|
||||
|
||||
let sidebar = document.getElementById("sidebar")
|
||||
sidebar.appendChild(button)
|
||||
room.button = button
|
||||
|
||||
if (!room.people) room.people = room.people = {}
|
||||
|
||||
let people = room.display.getElementsByClassName("roomDisplayPeople")[0]
|
||||
for (const user_id of room.users) {
|
||||
if (room.people[user_id]) continue
|
||||
let user = await getUser(user_id)
|
||||
let el = parseUser(user)
|
||||
people.appendChild(el)
|
||||
room.people[user_id] = el
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function onMessage(message) {
|
||||
let event = JSON.parse(message.data)
|
||||
switch (event.type) {
|
||||
case "message": {
|
||||
let room = data.rooms[event.room_id]
|
||||
if (!room) return
|
||||
let display = room.display
|
||||
let messages = display.getElementsByClassName('messages')[0]
|
||||
messages.prepend(await parseMessageImpl(event))
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
let room = data.rooms[event.room.room_id]
|
||||
if (!room) {
|
||||
// we are the user being added
|
||||
data.rooms[event.room.room_id] = event.room
|
||||
loadRoom(event.room.room_id)
|
||||
} else {
|
||||
let display = room.display
|
||||
let people = display.getElementsByClassName('roomDisplayPeople')[0]
|
||||
if (!room.people[event.user_id]) {
|
||||
let user = await getUser(event.user_id)
|
||||
let el = parseUser(user)
|
||||
people.appendChild(el)
|
||||
room.people[event.user_id] = el
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "leave": {
|
||||
let room = data.rooms[event.room_id]
|
||||
if (!room) return
|
||||
if (room.people[event.user_id]) {
|
||||
room.people[event.user_id].remove()
|
||||
delete room.people[event.user_id]
|
||||
}
|
||||
if (event.user_id == data.self.user_id) {
|
||||
room.display.remove()
|
||||
room.button.remove()
|
||||
delete data.rooms[event.room_id]
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "typing": {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn("unhandled event: " + message.data)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
let request = (await loadself());
|
||||
|
||||
if (request.json == undefined) {
|
||||
location.href = '/login'
|
||||
return
|
||||
}
|
||||
|
||||
data.self = request.json
|
||||
data.users[data.self.user_id] = data.self
|
||||
|
||||
render()
|
||||
|
||||
let rooms = (await chatlist());
|
||||
if (rooms.json === undefined) {
|
||||
alert(rooms.msg)
|
||||
} else {
|
||||
for (const room of rooms.json) {
|
||||
data.rooms[room.room_id] = room
|
||||
loadRoom(room.room_id)
|
||||
}
|
||||
}
|
||||
|
||||
let socket = new WebSocket(window.location.protocol.replace("http", "ws") + "//" + location.host + "/api/chat/connect")
|
||||
socket.addEventListener("message", onMessage);
|
||||
|
||||
}
|
||||
|
||||
|
||||
init()
|
|
@ -1,9 +1,9 @@
|
|||
import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth } from './main.js'
|
||||
import { postlike, postcomment, loadcommentspage } from './api.js';
|
||||
import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth, g, button } from './main.js'
|
||||
import { postlike, postcomment, loadcommentspage, chatsend, chatadd, chatleave } from './api.js';
|
||||
|
||||
window.parse = parse;
|
||||
|
||||
export function header(home, people, user_id) {
|
||||
export function header(home, people, chat, user_id) {
|
||||
return [
|
||||
div({id: 'header'},
|
||||
span({class: 'logo'},
|
||||
|
@ -21,6 +21,16 @@ export function header(home, people, user_id) {
|
|||
svg({viewBox: '0 0 28 28', fill: 'currentColor', height: '28', width: '28'},
|
||||
path({d: "M10.5 4.5c-2.272 0-2.75 1.768-2.75 3.25C7.75 9.542 8.983 11 10.5 11s2.75-1.458 2.75-3.25c0-1.482-.478-3.25-2.75-3.25zm0 8c-2.344 0-4.25-2.131-4.25-4.75C6.25 4.776 7.839 3 10.5 3s4.25 1.776 4.25 4.75c0 2.619-1.906 4.75-4.25 4.75zm9.5-6c-1.41 0-2.125.841-2.125 2.5 0 1.378.953 2.5 2.125 2.5 1.172 0 2.125-1.122 2.125-2.5 0-1.659-.715-2.5-2.125-2.5zm0 6.5c-1.999 0-3.625-1.794-3.625-4 0-2.467 1.389-4 3.625-4 2.236 0 3.625 1.533 3.625 4 0 2.206-1.626 4-3.625 4zm4.622 8a.887.887 0 00.878-.894c0-2.54-2.043-4.606-4.555-4.606h-1.86c-.643 0-1.265.148-1.844.413a6.226 6.226 0 011.76 4.336V21h5.621zm-7.122.562v-1.313a4.755 4.755 0 00-4.749-4.749H8.25A4.755 4.755 0 003.5 20.249v1.313c0 .518.421.938.937.938h12.125c.517 0 .938-.42.938-.938zM20.945 14C24.285 14 27 16.739 27 20.106a2.388 2.388 0 01-2.378 2.394h-5.81a2.44 2.44 0 01-2.25 1.5H4.437A2.44 2.44 0 012 21.562v-1.313A6.256 6.256 0 018.25 14h4.501a6.2 6.2 0 013.218.902A5.932 5.932 0 0119.084 14h1.861z"})
|
||||
)
|
||||
),
|
||||
a({id: 'chat', class: chat ? 'selected' : '', href: '/chat', 'aria-label': 'xssbook chat page'},
|
||||
svg({viewBox: '0 0 512 512', fill: 'currentColor', height: '28', width: '28'},
|
||||
g({transform: "translate(0.000000,512.000000) scale(0.100000,-0.100000)", fill: "#fffffff", stroke: "none"},
|
||||
path({d: "M1731 4799 c-240 -27 -467 -93 -687 -199 -992 -481 -1340 -1619 -768 -2512 l43 -66 -150 -469 c-82 -257 -149 -481 -149 -496 0 -73 75 -147 150 -147 31 0 215 89 725 350 l230 118 90 -35 c109 -42 279 -87 395 -104 83 -12 86 -14 147 -70 172 -159 313 -256 514 -354 507 -245 1103 -270 1644 -68 l81 30 449 -229 c291 -148 464 -232 491 -235 80 -10 164 63 164 143 0 15 -67 238 -149 496 l-150 469 43 67 c330 511 364 1151 90 1689 -268 524 -818 913 -1421 1003 -43 7 -83 15 -89 18 -7 4 -54 45 -106 92 -143 128 -266 212 -443 299 -215 107 -352 152 -580 191 -139 25 -430 34 -564 19z m407 -300 c123 -13 261 -43 377 -80 100 -33 300 -127 385 -182 l54 -35 -39 -7 c-273 -43 -442 -94 -645 -191 -911 -439 -1295 -1442 -887 -2317 25 -53 41 -97 36 -97 -6 0 -67 22 -136 50 -78 31 -141 50 -166 50 -32 0 -104 -33 -363 -165 -178 -91 -325 -165 -327 -165 -3 0 42 145 100 323 57 177 104 340 105 362 1 47 -6 63 -84 178 -107 157 -180 326 -220 510 -29 135 -31 396 -5 530 119 596 612 1070 1253 1206 186 40 380 50 562 30z m1220 -600 c223 -24 404 -78 607 -179 436 -217 742 -607 832 -1059 24 -119 24 -384 0 -504 -39 -194 -130 -405 -244 -563 -31 -43 -60 -94 -64 -112 -9 -42 -4 -61 114 -429 52 -161 93 -293 91 -293 -2 0 -149 74 -327 165 -263 134 -331 165 -365 165 -26 0 -82 -17 -149 -44 -528 -216 -1130 -170 -1608 124 -163 100 -335 258 -452 417 -115 155 -211 374 -250 570 -24 122 -24 384 0 506 106 530 514 974 1062 1155 239 79 508 108 753 81z"}),
|
||||
path({d: "M2488 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}),
|
||||
path({d: "M3088 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 75 -38 157 -14 198 58 27 49 28 91 2 142 -37 73 -127 99 -202 59z"}),
|
||||
path({d: "M3688 2539 c-43 -22 -78 -81 -78 -129 0 -50 35 -107 80 -130 49 -25 90 -25 138 -1 43 22 82 84 82 131 0 47 -39 109 -82 131 -47 24 -93 24 -140 -2z"})
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
a({class: 'pfp', id: 'profile', href: '/profile', 'aria-label': 'your xssbook profile'},
|
||||
|
@ -246,7 +256,7 @@ export function parseUser(user) {
|
|||
parse('Joined ' + parseDate(new Date(user.date)))
|
||||
),
|
||||
span({class: 'gtext'},
|
||||
parse('Gender :' + user.gender)
|
||||
parse('Gender:' + user.gender)
|
||||
),
|
||||
span({class: 'gtext'},
|
||||
parse('Birthday: ' + parseMonth(user.month) + ' ' + user.day + ', ' + user.year)
|
||||
|
@ -257,4 +267,185 @@ export function parseUser(user) {
|
|||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const stringToColor = (str) => {
|
||||
let hash = 0;
|
||||
str.split('').forEach(char => {
|
||||
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||
})
|
||||
let color = '#'
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xff
|
||||
color += value.toString(16).padStart(2, '0')
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
export function parseRoom(room, callback) {
|
||||
|
||||
let dspName = room.name[0].toUpperCase()
|
||||
let color = stringToColor(room.room_id + room.name + room.room_id)
|
||||
|
||||
return (
|
||||
div({class: 'room', onclick: () => {
|
||||
callback(room)
|
||||
}},
|
||||
div({class: 'room-icon ltext', style: `background-color: ${color}`},
|
||||
span({}, parse(dspName))
|
||||
),
|
||||
div({class: 'room-name ltext'},
|
||||
span({}, parse(room.name))
|
||||
),
|
||||
div({
|
||||
class: 'close',
|
||||
onclick: async () => {
|
||||
let request = (await chatleave(room.room_id))
|
||||
|
||||
if (request.status != 200) {
|
||||
alert(request.msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function parseRooms(rooms, callback) {
|
||||
let ret = []
|
||||
|
||||
for (const room of rooms) {
|
||||
ret.push(parseRoom(room, callback))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export function createMultiLineInput(attributes, onSubmit) {
|
||||
let area = span({
|
||||
...attributes,
|
||||
role: 'textbox',
|
||||
contenteditable: '',
|
||||
onkeydown: async (event) => {
|
||||
if (event.keyCode == 13 && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
let text = area.innerHTML.trim()
|
||||
.replaceAll("&", '&')
|
||||
.replaceAll("<", '<')
|
||||
.replaceAll(">", '>')
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'")
|
||||
|
||||
|
||||
text = text.replaceAll("\n", "<br>")
|
||||
if (text.length < 1) return
|
||||
if (await onSubmit(text)) {
|
||||
area.textContent = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
return area
|
||||
}
|
||||
|
||||
export function createSingleLineInput(attributes, onSubmit) {
|
||||
let area = span({
|
||||
...attributes,
|
||||
role: 'textbox',
|
||||
contenteditable: '',
|
||||
onkeydown: async (event) => {
|
||||
if (event.keyCode == 13 && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
let text = area.innerHTML.trim()
|
||||
text = text.replaceAll("\n", "<br>")
|
||||
if (text.length < 1) return
|
||||
if (await onSubmit(text)) {
|
||||
area.textContent = ''
|
||||
}
|
||||
} else if (event.keyCode == 13) {
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
})
|
||||
return area
|
||||
}
|
||||
|
||||
export function createRoomDisplay(room, loadMessageCallback) {
|
||||
let buttonEl = button({
|
||||
class: 'loadMessages input',
|
||||
style: 'flex-grow: 0',
|
||||
onclick: async () => {
|
||||
if (!await loadMessageCallback(room)) {
|
||||
buttonEl.remove()
|
||||
}
|
||||
}
|
||||
},
|
||||
parse('Load Previous')
|
||||
)
|
||||
|
||||
return (
|
||||
div({class: 'roomDisplay'},
|
||||
div({class: 'roomDisplayCenter'},
|
||||
buttonEl,
|
||||
div({class: 'messages'}),
|
||||
createMultiLineInput(
|
||||
{
|
||||
type: 'text',
|
||||
name: 'messageContent',
|
||||
class: 'messageContent input',
|
||||
},
|
||||
async (text) => {
|
||||
let result = (await chatsend(text, room.room_id))
|
||||
|
||||
if (result.status != 201) {
|
||||
alert(result.msg)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
div({class: 'roomDisplayPeople'},
|
||||
span({class: 'ltext'},
|
||||
parse("People"),
|
||||
),
|
||||
createSingleLineInput(
|
||||
{
|
||||
type: 'text',
|
||||
name: 'addUser',
|
||||
class: 'addUser input',
|
||||
style: 'flex-grow: 0; width: 80%'
|
||||
},
|
||||
async (text) => {
|
||||
let result = (await chatadd(text, room.room_id))
|
||||
if (result.status != 200) {
|
||||
alert(result.msg)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function parseMessage(message, user) {
|
||||
return (
|
||||
div({class: 'message'},
|
||||
a({class: 'message-pfp', href: `/profile?id=${message.user_id}`},
|
||||
pfp(message.user_id)
|
||||
),
|
||||
div({class: 'message-content'},
|
||||
span({class: 'message-name ltext'},
|
||||
parse(user.firstname + ' ' + user.lastname)
|
||||
),
|
||||
p({class: 'message-text ltext'},
|
||||
parse(message.content)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ function render() {
|
|||
|
||||
let new_body =
|
||||
body({},
|
||||
...header(true, false, data.self.user_id),
|
||||
...header(true, false, false, data.self.user_id),
|
||||
div({id: 'create'},
|
||||
div({class: 'create'},
|
||||
a({class: 'pfp', href: '/profile'},
|
||||
|
@ -44,7 +44,7 @@ function render() {
|
|||
),
|
||||
button({class: 'pfp'},
|
||||
p({class: 'gtext', onclick: () => document.getElementById('popup').classList.remove('hidden')},
|
||||
parse(`What' on your mind, ${data.self.firstname}`)
|
||||
parse(`What's on your mind, ${data.self.firstname}`)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -113,7 +113,9 @@ async function load() {
|
|||
const posts = (await loadpostspage(page)).json
|
||||
|
||||
if (posts.length === 0) {
|
||||
document.getElementById('load').remove()
|
||||
let load = document.getElementById('load')
|
||||
if (load)
|
||||
load.remove()
|
||||
return []
|
||||
} else {
|
||||
page++
|
||||
|
@ -134,17 +136,6 @@ async function load() {
|
|||
async function init() {
|
||||
|
||||
let request = (await loadself());
|
||||
|
||||
if (request.status === 429) {
|
||||
let new_body =
|
||||
body({},
|
||||
...header(true, false)
|
||||
)
|
||||
|
||||
document.body.replaceWith(new_body)
|
||||
throw new Error("Rate limited");
|
||||
}
|
||||
|
||||
data.self = request.json
|
||||
|
||||
if (request.json == undefined) {
|
||||
|
@ -161,4 +152,4 @@ async function init() {
|
|||
}
|
||||
|
||||
|
||||
init()
|
||||
init()
|
||||
|
|
|
@ -83,6 +83,10 @@ export function path(attrs, ...children) {
|
|||
return createElementNS("path", attrs, ...children)
|
||||
}
|
||||
|
||||
export function g(attrs, ...children) {
|
||||
return createElementNS("g", attrs, ...children)
|
||||
}
|
||||
|
||||
export function svg(attrs, ...children) {
|
||||
return createElementNS("svg", attrs, ...children)
|
||||
}
|
||||
|
@ -99,10 +103,6 @@ export function parse(html) {
|
|||
return document.createRange().createContextualFragment(html);
|
||||
}
|
||||
|
||||
export function pfpl(id) {
|
||||
|
||||
}
|
||||
|
||||
export function pfp(id) {
|
||||
return img('pfp', {src: `/image/avatar?user_id=${id}`})
|
||||
}
|
||||
|
@ -142,4 +142,4 @@ export function crawl(key, object) {
|
|||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ function render() {
|
|||
|
||||
let new_body =
|
||||
body({},
|
||||
...header(false, true, data.self.user_id),
|
||||
...header(false, true, false, data.self.user_id),
|
||||
div({id: 'users'},
|
||||
...data.users.map(u => parseUser(u))
|
||||
),
|
||||
|
@ -49,17 +49,6 @@ async function load() {
|
|||
async function init() {
|
||||
|
||||
let request = (await loadself());
|
||||
|
||||
if (request.status === 429) {
|
||||
let new_body =
|
||||
body({},
|
||||
...header(true, false, data.self.user_id)
|
||||
)
|
||||
|
||||
document.body.replaceWith(new_body)
|
||||
throw new Error("Rate limited");
|
||||
}
|
||||
|
||||
if (request.json == undefined) {
|
||||
location.href = '/login'
|
||||
return
|
||||
|
@ -73,4 +62,4 @@ async function init() {
|
|||
render()
|
||||
}
|
||||
|
||||
init()
|
||||
init()
|
||||
|
|
|
@ -107,7 +107,7 @@ async function render() {
|
|||
|
||||
let new_body =
|
||||
body({},
|
||||
...header(false, false, data.self.user_id),
|
||||
...header(false, false, false, data.self.user_id),
|
||||
div({id: 'top'},
|
||||
div({id: 'banner'},
|
||||
div({class: 'bg'},
|
||||
|
@ -223,7 +223,7 @@ async function render() {
|
|||
parse('Email: ' + data.user.email)
|
||||
),
|
||||
span({class: 'gtext bold'},
|
||||
parse('Gender ' + data.user.gender)
|
||||
parse('Gender: ' + data.user.gender)
|
||||
),
|
||||
span({class: 'gtext bold'},
|
||||
parse('Birthday: ' + parseMonth(data.user.month) + ' ' + data.user.day + ', ' + data.user.year)
|
||||
|
@ -362,4 +362,4 @@ async function init() {
|
|||
render()
|
||||
}
|
||||
|
||||
init()
|
||||
init()
|
||||
|
|
512
src/api/chat.rs
Normal file
512
src/api/chat.rs
Normal file
|
@ -0,0 +1,512 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{response::Response, Router, routing::{post, patch, delete, get}, extract::{ws::Message, WebSocketUpgrade}};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{Mutex, mpsc::{Sender, self}};
|
||||
use crate::{
|
||||
public::docs::{EndpointDocumentation, EndpointMethod},
|
||||
types::{
|
||||
extract::{AuthorizedUser, Check, CheckResult, Database, Json, Log},
|
||||
http::ResponseCode,
|
||||
chat::{ChatRoom, ChatEvent}, user::User,
|
||||
},
|
||||
};
|
||||
use std::collections::hash_map::Values;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static!(
|
||||
static ref CONNECTIONS: Mutex<HashMap<u64, ConnectionPool>> = Mutex::new(HashMap::new());
|
||||
);
|
||||
|
||||
struct ConnectionPool {
|
||||
inner: HashMap<usize, Sender<ChatEvent>>,
|
||||
index: usize
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: HashMap::new(),
|
||||
index: 0
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, send: Sender<ChatEvent>) -> usize {
|
||||
let idx = self.index;
|
||||
self.index += 1;
|
||||
self.inner.insert(idx, send);
|
||||
idx
|
||||
}
|
||||
|
||||
fn del(&mut self, idx: &usize) {
|
||||
self.inner.remove(idx);
|
||||
}
|
||||
|
||||
fn values(&self) -> Values<'_, usize, Sender<ChatEvent>> {
|
||||
self.inner.values()
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_event(event: ChatEvent, room: &ChatRoom) {
|
||||
for user in &room.users {
|
||||
let lock = CONNECTIONS.lock().await;
|
||||
let Some(connection) = lock.get(&user) else {
|
||||
continue
|
||||
};
|
||||
for channel in connection.values() {
|
||||
channel.send(event.clone()).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const CHAT_LIST: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/list",
|
||||
method: EndpointMethod::Post,
|
||||
description: "Returns the rooms you are in",
|
||||
body: None,
|
||||
responses: &[
|
||||
(201, "Returns rooms in a list"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to retrieve rooms"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
async fn list (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
_: Log
|
||||
) -> Response {
|
||||
let Ok(rooms) = ChatRoom::from_user_id(&db, user.user_id) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to retrieve rooms")
|
||||
};
|
||||
|
||||
let Ok(json) = serde_json::to_string(&rooms) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to retrieve rooms")
|
||||
};
|
||||
|
||||
ResponseCode::Success.json(&json)
|
||||
}
|
||||
|
||||
pub const CHAT_CREATE: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/create",
|
||||
method: EndpointMethod::Post,
|
||||
description: "Creates a new room",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"name" : "Funny memes"
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully created room"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to create room"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RoomCreateRequest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Check for RoomCreateRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Self::assert_length(
|
||||
&self.name,
|
||||
1,
|
||||
255,
|
||||
"Room names must be between 1-255 characters long",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<RoomCreateRequest>,
|
||||
) -> Response {
|
||||
let Ok(room) = ChatRoom::new(&db, vec![user.user_id], body.name) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to create room")
|
||||
};
|
||||
|
||||
for user in &room.users {
|
||||
send_event(ChatEvent::Add {
|
||||
user_id: *user,
|
||||
room: room.clone()
|
||||
}, &room).await;
|
||||
}
|
||||
|
||||
let Ok(json) = serde_json::to_string(&room) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to create room")
|
||||
};
|
||||
|
||||
ResponseCode::Created.json(&json)
|
||||
}
|
||||
|
||||
pub const CHAT_ADD: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/add",
|
||||
method: EndpointMethod::Patch,
|
||||
description: "Adds a user to a room",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"room_id": 69,
|
||||
"email" : "joebide@house.gov"
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully added user"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to add user"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AddUserRequest {
|
||||
room_id: u64,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl Check for AddUserRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn add (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<AddUserRequest>,
|
||||
) -> Response {
|
||||
|
||||
let Ok(to_add) = User::from_email(&db, &body.email) else {
|
||||
return ResponseCode::BadRequest.text("User does not exist")
|
||||
};
|
||||
|
||||
let Ok(mut room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
|
||||
return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
|
||||
};
|
||||
|
||||
if room.users.contains(&to_add.user_id) {
|
||||
return ResponseCode::BadRequest.text("User is already in the room")
|
||||
}
|
||||
|
||||
let Ok(success) = room.add_user(&db, to_add.user_id) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to add user")
|
||||
};
|
||||
|
||||
if !success {
|
||||
return ResponseCode::BadRequest.text("User is already in the room")
|
||||
}
|
||||
|
||||
room.users.push(to_add.user_id);
|
||||
|
||||
send_event(ChatEvent::Add {
|
||||
user_id: to_add.user_id,
|
||||
room: room.clone()
|
||||
}, &room).await;
|
||||
|
||||
ResponseCode::Success.text("Successfully added user")
|
||||
}
|
||||
|
||||
pub const CHAT_LEAVE: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/leave",
|
||||
method: EndpointMethod::Delete,
|
||||
description: "Leaves a room",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"room_id": 69
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully left room"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to leave a room"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LeaveRoomRequest {
|
||||
room_id: u64,
|
||||
}
|
||||
|
||||
impl Check for LeaveRoomRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn leave (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<LeaveRoomRequest>,
|
||||
) -> Response {
|
||||
|
||||
let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
|
||||
return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
|
||||
};
|
||||
|
||||
let Ok(success) = room.remove_user(&db, user.user_id) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to leave room")
|
||||
};
|
||||
|
||||
if !success {
|
||||
return ResponseCode::BadRequest.text("You are currently not in this room (how did this happen?)")
|
||||
}
|
||||
|
||||
send_event(ChatEvent::Leave {
|
||||
user_id: user.user_id,
|
||||
room_id: room.room_id
|
||||
}, &room).await;
|
||||
|
||||
ResponseCode::Success.text("Successfully left room")
|
||||
}
|
||||
|
||||
pub const CHAT_SEND: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/send",
|
||||
method: EndpointMethod::Post,
|
||||
description: "Send a message to a room",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"room_id": 420,
|
||||
"content" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully sent message"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to send message"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SendMessageRequest {
|
||||
room_id: u64,
|
||||
content: String
|
||||
}
|
||||
|
||||
impl Check for SendMessageRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Self::assert_length(
|
||||
&self.content,
|
||||
1,
|
||||
500,
|
||||
"Messages must be between 1-500 length"
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<SendMessageRequest>,
|
||||
) -> Response {
|
||||
|
||||
let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
|
||||
return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
|
||||
};
|
||||
|
||||
let Ok(msg) = room.send_message(&db, user.user_id, body.content) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to send message")
|
||||
};
|
||||
|
||||
send_event(ChatEvent::Message {
|
||||
user_id: msg.user_id,
|
||||
room_id: msg.room_id,
|
||||
message_id: msg.message_id,
|
||||
content: msg.content,
|
||||
date: msg.date
|
||||
}, &room).await;
|
||||
|
||||
ResponseCode::Created.text("Successfully sent message")
|
||||
}
|
||||
|
||||
pub const CHAT_LOAD: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/load",
|
||||
method: EndpointMethod::Post,
|
||||
description: "Get a page of historic room messages starting before given message id",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"room_id": 69,
|
||||
"newest_msg": 400,
|
||||
"page": 3
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully sent message"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to send message"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoadMessagesRequest {
|
||||
room_id: u64,
|
||||
newest_msg: u64,
|
||||
page: u64
|
||||
}
|
||||
|
||||
impl Check for LoadMessagesRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn load (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<LoadMessagesRequest>,
|
||||
) -> Response {
|
||||
|
||||
let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
|
||||
return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
|
||||
};
|
||||
|
||||
let Ok(msgs) = room.load_old_chat_messages(&db, body.newest_msg, body.page) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to load messages")
|
||||
};
|
||||
|
||||
let Ok(json) = serde_json::to_string(&msgs) else {
|
||||
return ResponseCode::InternalServerError.text("Failed to load messages")
|
||||
};
|
||||
|
||||
ResponseCode::Created.json(&json)
|
||||
}
|
||||
|
||||
pub const CHAT_TYPING: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/typing",
|
||||
method: EndpointMethod::Post,
|
||||
description: "Set if your typing in a given room",
|
||||
body: Some(
|
||||
r#"
|
||||
{
|
||||
"room_id": 69,
|
||||
}
|
||||
"#,
|
||||
),
|
||||
responses: &[
|
||||
(201, "Successfully sent typing indicator"),
|
||||
(400, "Body does not match parameters"),
|
||||
(401, "Unauthorized"),
|
||||
(500, "Failed to send typing indicator"),
|
||||
],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TypingRequest {
|
||||
room_id: u64,
|
||||
}
|
||||
|
||||
impl Check for TypingRequest {
|
||||
fn check(&self) -> CheckResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn typing (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
Database(db): Database,
|
||||
Json(body): Json<TypingRequest>,
|
||||
) -> Response {
|
||||
|
||||
let Ok(room) = ChatRoom::from_user_and_room_id(&db, user.user_id, body.room_id) else {
|
||||
return ResponseCode::BadRequest.text("Room doesnt exist or you are not in it")
|
||||
};
|
||||
|
||||
send_event(ChatEvent::Typing {
|
||||
user_id: user.user_id,
|
||||
room_id: room.room_id,
|
||||
}, &room).await;
|
||||
|
||||
ResponseCode::Success.text("Successfully sent typing indicator")
|
||||
}
|
||||
|
||||
pub const CHAT_CONNECT: EndpointDocumentation = EndpointDocumentation {
|
||||
uri: "/api/chat/connect",
|
||||
method: EndpointMethod::Get,
|
||||
description: "Start a websocket connection for chat events",
|
||||
body: None,
|
||||
responses: &[],
|
||||
cookie: Some("auth"),
|
||||
};
|
||||
|
||||
async fn connect (
|
||||
AuthorizedUser(user): AuthorizedUser,
|
||||
ws: WebSocketUpgrade
|
||||
) -> Response {
|
||||
ws.on_upgrade(|mut ws| async move {
|
||||
let user = user;
|
||||
let (send, mut recv) = mpsc::channel::<ChatEvent>(20);
|
||||
let id: usize;
|
||||
{
|
||||
let mut lock = CONNECTIONS.lock().await;
|
||||
match lock.get_mut(&user.user_id) {
|
||||
Some(pool) => {
|
||||
id = pool.add(send);
|
||||
},
|
||||
None => {
|
||||
let mut pool = ConnectionPool::new();
|
||||
id = pool.add(send);
|
||||
lock.insert(user.user_id, pool);
|
||||
}
|
||||
};
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
m = ws.recv() => {
|
||||
let Some(Ok(_)) = m else {
|
||||
break;
|
||||
};
|
||||
}
|
||||
s = recv.recv() => {
|
||||
let Some(msg) = s else {
|
||||
break;
|
||||
};
|
||||
if let Ok(string) = serde_json::to_string(&msg) {
|
||||
ws.send(Message::Text(string)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut lock = CONNECTIONS.lock().await;
|
||||
if let Some(conn) = lock.get_mut(&user.user_id) {
|
||||
conn.del(&id);
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/create", post(create))
|
||||
.route("/list", post(list))
|
||||
.route("/add", patch(add))
|
||||
.route("/leave", delete(leave))
|
||||
.route("/send", post(send))
|
||||
.route("/load", post(load))
|
||||
.route("/typing", post(typing))
|
||||
.route("/connect", get(connect))
|
||||
}
|
|
@ -1,32 +1,20 @@
|
|||
use crate::types::extract::{RouterURI, self};
|
||||
use axum::{
|
||||
error_handling::HandleErrorLayer,
|
||||
BoxError, Extension, Router, middleware,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_governor::{
|
||||
errors::display_error, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
|
||||
GovernorLayer,
|
||||
};
|
||||
|
||||
pub mod chat;
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
pub mod posts;
|
||||
pub mod users;
|
||||
|
||||
pub use auth::RegistrationRequet;
|
||||
use axum::{Extension, Router, middleware};
|
||||
|
||||
pub fn router() -> Router {
|
||||
let governor_conf = Box::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.burst_size(15)
|
||||
.per_second(1)
|
||||
.key_extractor(SmartIpKeyExtractor)
|
||||
.finish()
|
||||
.expect("Failed to create rate limiter"),
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.nest(
|
||||
"/chat",
|
||||
chat::router().layer(Extension(RouterURI("/api/chat"))),
|
||||
)
|
||||
.nest(
|
||||
"/admin",
|
||||
admin::router().layer(Extension(RouterURI("/api/admin"))),
|
||||
|
@ -43,14 +31,5 @@ pub fn router() -> Router {
|
|||
"/posts",
|
||||
posts::router().layer(Extension(RouterURI("/api/posts"))),
|
||||
)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|e: BoxError| async move {
|
||||
display_error(e)
|
||||
}))
|
||||
.layer(GovernorLayer {
|
||||
config: Box::leak(governor_conf),
|
||||
}),
|
||||
)
|
||||
.layer(middleware::from_fn(extract::connect))
|
||||
}
|
||||
|
|
211
src/database/chat.rs
Normal file
211
src/database/chat.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH, Duration};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::types::chat::{ChatRoom, ChatMessage};
|
||||
|
||||
use super::Database;
|
||||
|
||||
impl Database {
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn init_chat(&self) -> Result<(), rusqlite::Error> {
|
||||
let sql = "
|
||||
CREATE TABLE IF NOT EXISTS chat_rooms (
|
||||
room_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
";
|
||||
self.0.execute(sql, ())?;
|
||||
|
||||
let sql2 = "
|
||||
CREATE TABLE IF NOT EXISTS chat_users (
|
||||
room_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(room_id) REFERENCES chat_rooms(room_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(user_id),
|
||||
PRIMARY KEY (room_id, user_id)
|
||||
);
|
||||
";
|
||||
self.0.execute(sql2, ())?;
|
||||
|
||||
let sql3 = "
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
message_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
room_id INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
content VARCHAR(500) NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(user_id),
|
||||
FOREIGN KEY(room_id) REFERENCES chat_rooms(room_id)
|
||||
);
|
||||
";
|
||||
self.0.execute(sql3, ())?;
|
||||
|
||||
let sql4 = "CREATE INDEX IF NOT EXISTS chat_message_ids ON chat_messages(room_id);";
|
||||
self.0.execute(sql4, ())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn get_rooms(&self, user_id: u64) -> Result<Vec<ChatRoom>, rusqlite::Error> {
|
||||
tracing::trace!("Retrieving rooms");
|
||||
let mut stmt = self.0.prepare(
|
||||
"
|
||||
SELECT * FROM chat_rooms
|
||||
WHERE room_id IN (
|
||||
SELECT room_id
|
||||
FROM chat_users
|
||||
WHERE user_id = ?
|
||||
);
|
||||
",
|
||||
)?;
|
||||
|
||||
let row = stmt.query_map([user_id], |row| {
|
||||
let room_id: u64 = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
|
||||
let mut stmt2 = self.0.prepare(
|
||||
"
|
||||
SELECT user_id FROM chat_users
|
||||
WHERE room_id = ?;
|
||||
"
|
||||
)?;
|
||||
|
||||
let users = stmt2.query_map([room_id], |row2| {
|
||||
Ok(row2.get(0)?)
|
||||
})?.into_iter().flatten().collect();
|
||||
|
||||
let room = ChatRoom {
|
||||
room_id,
|
||||
users,
|
||||
name
|
||||
};
|
||||
|
||||
Ok(room)
|
||||
})?;
|
||||
|
||||
Ok(row.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn create_room(&self, users: Vec<u64>, name: String) -> Result<ChatRoom, rusqlite::Error> {
|
||||
tracing::trace!("Creating new room");
|
||||
let mut stmt = self.0.prepare(
|
||||
"INSERT INTO chat_rooms (name) VALUES (?) RETURNING *;"
|
||||
)?;
|
||||
let mut room = stmt.query_row([name], |row| {
|
||||
let room_id = row.get(0)?;
|
||||
let name = row.get(1)?;
|
||||
Ok(ChatRoom {
|
||||
room_id,
|
||||
users: Vec::new(),
|
||||
name
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut stmt2 = self.0.prepare(
|
||||
"INSERT INTO chat_users (room_id, user_id) VALUES (?, ?);"
|
||||
)?;
|
||||
|
||||
for user_id in users {
|
||||
stmt2.execute([room.room_id, user_id])?;
|
||||
room.users.push(user_id);
|
||||
}
|
||||
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_user_to_room(&self, room_id: u64, user_id: u64) -> Result<bool, rusqlite::Error> {
|
||||
tracing::trace!("Adding user to room");
|
||||
let mut stmt = self.0.prepare(
|
||||
"INSERT OR REPLACE INTO chat_users (room_id, user_id) VALUES(?,?);"
|
||||
)?;
|
||||
|
||||
let changes = stmt.execute([room_id, user_id])?;
|
||||
|
||||
Ok(changes == 1)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn remove_user_from_room(&self, room_id: u64, user_id: u64) -> Result<bool, rusqlite::Error> {
|
||||
tracing::trace!("Removing user from room");
|
||||
let mut stmt = self.0.prepare(
|
||||
"DELETE FROM chat_users WHERE room_id = ? AND user_id = ?;"
|
||||
)?;
|
||||
|
||||
let changes = stmt.execute([room_id, user_id])?;
|
||||
|
||||
Ok(changes == 1)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn create_message(&self, room_id: u64, user_id: u64, content: String) -> Result<ChatMessage, rusqlite::Error> {
|
||||
tracing::trace!("Creating new chat message");
|
||||
let date = u64::try_from(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_millis(),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut stmt = self.0.prepare(
|
||||
"INSERT INTO chat_messages (user_id, room_id, date, content) VALUES (?,?,?,?) RETURNING *;"
|
||||
)?;
|
||||
|
||||
let msg = stmt.query_row((user_id, room_id, date, content), |row| {
|
||||
let message_id = row.get(0)?;
|
||||
let user_id = row.get(1)?;
|
||||
let room_id = row.get(2)?;
|
||||
let date = row.get(3)?;
|
||||
let content = row.get(4)?;
|
||||
|
||||
Ok(ChatMessage {
|
||||
message_id,
|
||||
room_id,
|
||||
user_id,
|
||||
date,
|
||||
content
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn load_old_chat_messages(&self, room_id: u64, newest_message: u64, page: u64) -> Result<Vec<ChatMessage>, rusqlite::Error> {
|
||||
tracing::trace!("Loading old chat messages");
|
||||
let mut stmt = self.0.prepare(
|
||||
"
|
||||
SELECT * FROM chat_messages
|
||||
WHERE room_id = ?
|
||||
AND message_id < ?
|
||||
ORDER BY message_id DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
"
|
||||
)?;
|
||||
|
||||
let messages = stmt.query_map((room_id, newest_message, 20, 20 * page), |row| {
|
||||
let message_id = row.get(0)?;
|
||||
let user_id = row.get(1)?;
|
||||
let room_id = row.get(2)?;
|
||||
let date = row.get(3)?;
|
||||
let content = row.get(4)?;
|
||||
|
||||
Ok(ChatMessage {
|
||||
message_id,
|
||||
room_id,
|
||||
user_id,
|
||||
date,
|
||||
content
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(messages.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use rusqlite::Connection;
|
||||
use tracing::instrument;
|
||||
|
||||
pub mod chat;
|
||||
pub mod comments;
|
||||
pub mod friends;
|
||||
pub mod likes;
|
||||
|
@ -32,5 +33,6 @@ pub fn init() -> Result<(), rusqlite::Error> {
|
|||
db.init_likes()?;
|
||||
db.init_comments()?;
|
||||
db.init_friends()?;
|
||||
db.init_chat()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ async fn main() {
|
|||
tracing_subscriber::registry()
|
||||
.with(
|
||||
fmt_layer
|
||||
.with_filter(LevelFilter::TRACE)
|
||||
.with_filter(LevelFilter::INFO)
|
||||
.with_filter(filter_fn(|metadata| {
|
||||
metadata.target().starts_with("xssbook")
|
||||
})),
|
||||
|
|
|
@ -3,24 +3,28 @@ use lazy_static::lazy_static;
|
|||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
api::{admin, auth, posts, users},
|
||||
api::{admin, auth, posts, users, chat},
|
||||
types::http::ResponseCode,
|
||||
};
|
||||
|
||||
use super::console::beautify;
|
||||
|
||||
pub enum EndpointMethod {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete
|
||||
}
|
||||
|
||||
impl ToString for EndpointMethod {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Get => "GET".to_owned(),
|
||||
Self::Post => "POST".to_owned(),
|
||||
Self::Put => "PUT".to_owned(),
|
||||
Self::Patch => "PATCH".to_owned(),
|
||||
Self::Delete => "DELETE".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +143,14 @@ pub async fn init() {
|
|||
users::USERS_FOLLOW,
|
||||
users::USERS_FOLLOW_STATUS,
|
||||
users::USERS_FRIENDS,
|
||||
chat::CHAT_LIST,
|
||||
chat::CHAT_CREATE,
|
||||
chat::CHAT_ADD,
|
||||
chat::CHAT_LEAVE,
|
||||
chat::CHAT_SEND,
|
||||
chat::CHAT_LOAD,
|
||||
chat::CHAT_TYPING,
|
||||
chat::CHAT_CONNECT,
|
||||
admin::ADMIN_AUTH,
|
||||
admin::ADMIN_QUERY,
|
||||
admin::ADMIN_POSTS,
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
use axum::{
|
||||
body::Body,
|
||||
error_handling::HandleErrorLayer,
|
||||
headers::HeaderName,
|
||||
http::{HeaderValue, Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
BoxError, Router,
|
||||
};
|
||||
use tower::{ServiceBuilder, ServiceExt};
|
||||
use tower_governor::{
|
||||
errors::display_error, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
|
||||
GovernorLayer,
|
||||
Router,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
use crate::types::http::ResponseCode;
|
||||
|
@ -23,15 +18,6 @@ pub mod file;
|
|||
pub mod pages;
|
||||
|
||||
pub fn router() -> Router {
|
||||
let governor_conf = Box::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.burst_size(30)
|
||||
.per_second(1)
|
||||
.key_extractor(SmartIpKeyExtractor)
|
||||
.finish()
|
||||
.expect("Failed to create rate limiter"),
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.nest("/", pages::router())
|
||||
.route("/favicon.ico", get(file::favicon))
|
||||
|
@ -42,15 +28,6 @@ pub fn router() -> Router {
|
|||
.route("/image/*path", get(file::image))
|
||||
.route("/image/avatar", get(file::avatar))
|
||||
.route("/image/banner", get(file::banner))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|e: BoxError| async move {
|
||||
display_error(e)
|
||||
}))
|
||||
.layer(GovernorLayer {
|
||||
config: Box::leak(governor_conf),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn serve(path: &str) -> Response {
|
||||
|
|
|
@ -66,6 +66,10 @@ async fn forgot(UserAgent(agent): UserAgent, _: Log) -> Response {
|
|||
Redirect::to("https://www.youtube.com/watch?v=dQw4w9WgXcQ").into_response()
|
||||
}
|
||||
|
||||
async fn chat() -> Response {
|
||||
super::serve("/chat.html").await
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
|
@ -79,4 +83,5 @@ pub fn router() -> Router {
|
|||
.route("/admin", get(admin))
|
||||
.route("/docs", get(api))
|
||||
.route("/forgot", get(forgot))
|
||||
.route("/chat", get(chat))
|
||||
}
|
||||
|
|
129
src/types/chat.rs
Normal file
129
src/types/chat.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use tracing::instrument;
|
||||
use crate::{types::http::{ResponseCode, Result}, database::Database};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct ChatRoom {
|
||||
pub room_id: u64,
|
||||
pub users: Vec<u64>,
|
||||
pub name: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct ChatMessage {
|
||||
pub message_id: u64,
|
||||
pub user_id: u64,
|
||||
pub room_id: u64,
|
||||
pub date: u64,
|
||||
pub content: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatEvent {
|
||||
#[serde(rename = "message")]
|
||||
Message {
|
||||
user_id: u64,
|
||||
message_id: u64,
|
||||
room_id: u64,
|
||||
content: String,
|
||||
date: u64
|
||||
},
|
||||
|
||||
#[serde(rename = "add")]
|
||||
Add {
|
||||
user_id: u64,
|
||||
room: ChatRoom
|
||||
},
|
||||
|
||||
#[serde(rename = "leave")]
|
||||
Leave {
|
||||
user_id: u64,
|
||||
room_id: u64
|
||||
},
|
||||
|
||||
#[serde(rename = "typing")]
|
||||
Typing {
|
||||
user_id: u64,
|
||||
room_id: u64
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatRoom {
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn new(db: &Database, users: Vec<u64>, name: String) -> Result<Self> {
|
||||
let Ok(room) = db.create_room(users, name) else {
|
||||
tracing::error!("Failed to create room");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to create room"))
|
||||
};
|
||||
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn from_user_id(db: &Database, user_id: u64) -> Result<Vec<Self>> {
|
||||
let Ok(rooms) = db.get_rooms(user_id) else {
|
||||
tracing::error!("Failed to get rooms");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to get rooms"))
|
||||
};
|
||||
|
||||
Ok(rooms)
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn from_user_and_room_id(db: &Database, user_id: u64, room_id: u64) -> Result<Self> {
|
||||
let Ok(rooms) = db.get_rooms(user_id) else {
|
||||
tracing::error!("Failed to get room");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to get room"))
|
||||
};
|
||||
|
||||
for room in rooms {
|
||||
if room.room_id == room_id {
|
||||
return Ok(room);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(ResponseCode::BadRequest.text("Room doesnt exist or you are not in it"))
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn add_user(&self, db: &Database, user_id: u64) -> Result<bool> {
|
||||
let Ok(success) = db.add_user_to_room(self.room_id, user_id) else {
|
||||
tracing::error!("Failed to add user to room");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to add user to room"))
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn remove_user(&self, db: &Database, user_id: u64) -> Result<bool> {
|
||||
let Ok(success) = db.remove_user_from_room(self.room_id, user_id) else {
|
||||
tracing::error!("Failed to remove user from room");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to remove user from room"))
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn send_message(&self, db: &Database, user_id: u64, content: String) -> Result<ChatMessage> {
|
||||
let Ok(msg) = db.create_message(self.room_id, user_id, content) else {
|
||||
tracing::error!("Failed to create messgae");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to create message"))
|
||||
};
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
pub fn load_old_chat_messages(&self, db: &Database, newest_message: u64, page: u64) -> Result<Vec<ChatMessage>> {
|
||||
let Ok(msgs) = db.load_old_chat_messages(self.room_id, newest_message, page) else {
|
||||
tracing::error!("Failed to load messgaes");
|
||||
return Err(ResponseCode::InternalServerError.text("Failed to load messages"))
|
||||
};
|
||||
|
||||
Ok(msgs)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod chat;
|
||||
pub mod comment;
|
||||
pub mod extract;
|
||||
pub mod http;
|
||||
|
|
Loading…
Reference in a new issue