Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca3f6c0579 | |||
decc9e5139 | |||
d27bd782ce | |||
4defb58f2a | |||
ba42fc066f | |||
2cd35fb3b6 | |||
b9af6f47f3 | |||
73732a2a44 | |||
43ed9756dc | |||
8bb247af3b | |||
ed05f5a092 | |||
4f09333cd7 | |||
31bf8c802e | |||
e4f9bd7b8d | |||
83a667347d | |||
499a56aed4 | |||
928907086c |
120
Cargo.lock
generated
120
Cargo.lock
generated
@ -173,9 +173,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.67"
|
version = "1.0.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
|
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@ -196,9 +196,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_fn"
|
name = "const_fn"
|
||||||
version = "0.4.7"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "402da840495de3f976eaefc3485b7f5eb5b0bf9761f9a47be27fe975b3b8c2ec"
|
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
@ -315,9 +315,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253"
|
checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -330,9 +330,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25"
|
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -340,15 +340,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815"
|
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10f6cb7042eda00f0049b1d2080aa4b93442997ee507eb3828e8bd7577f94c9d"
|
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@ -357,9 +357,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04"
|
checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
@ -378,10 +378,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668c6733a182cd7deb4f1de7ba3bf2120823835b3bcfbeacf7d2c4a773c1bb8b"
|
checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
"proc-macro-hack",
|
"proc-macro-hack",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -390,22 +391,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23"
|
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc"
|
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.14"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025"
|
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@ -478,21 +480,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437"
|
checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9"
|
checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.7"
|
version = "0.14.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54"
|
checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@ -573,9 +575,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.50"
|
version = "0.3.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
|
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@ -601,13 +603,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
|
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libreddit"
|
name = "libreddit"
|
||||||
version = "0.11.2"
|
version = "0.14.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
@ -718,9 +720,9 @@ checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.2"
|
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 = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
@ -805,9 +807,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.26"
|
version = "1.0.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
@ -980,18 +982,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.125"
|
version = "1.0.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.125"
|
version = "1.0.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1203,9 +1205,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5"
|
checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -1223,9 +1225,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57"
|
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1245,9 +1247,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.6.6"
|
version = "0.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e"
|
checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -1361,9 +1363,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
|
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@ -1371,9 +1373,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
|
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -1386,9 +1388,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
|
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@ -1396,9 +1398,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
|
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1409,15 +1411,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.73"
|
version = "0.2.74"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
|
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.50"
|
version = "0.3.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
|
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -3,7 +3,7 @@ name = "libreddit"
|
|||||||
description = " Alternative private front-end to Reddit"
|
description = " Alternative private front-end to Reddit"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://github.com/spikecodes/libreddit"
|
repository = "https://github.com/spikecodes/libreddit"
|
||||||
version = "0.11.2"
|
version = "0.14.6"
|
||||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@ -13,13 +13,13 @@ async-recursion = "0.3.2"
|
|||||||
cached = "0.23.0"
|
cached = "0.23.0"
|
||||||
clap = { version = "2.33.3", default-features = false }
|
clap = { version = "2.33.3", default-features = false }
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
serde = { version = "1.0.125", features = ["derive"] }
|
serde = { version = "1.0.126", features = ["derive"] }
|
||||||
cookie = "0.15.0"
|
cookie = "0.15.0"
|
||||||
futures-lite = "1.11.3"
|
futures-lite = "1.11.3"
|
||||||
hyper = { version = "0.14.7", features = ["full"] }
|
hyper = { version = "0.14.8", features = ["full"] }
|
||||||
hyper-rustls = "0.22.1"
|
hyper-rustls = "0.22.1"
|
||||||
route-recognizer = "0.3.0"
|
route-recognizer = "0.3.0"
|
||||||
serde_json = "1.0.64"
|
serde_json = "1.0.64"
|
||||||
tokio = { version = "1.5.0", features = ["full"] }
|
tokio = { version = "1.6.0", features = ["full"] }
|
||||||
time = "0.2.26"
|
time = "0.2.26"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
|
45
README.md
45
README.md
@ -194,6 +194,32 @@ Once installed, deploy Libreddit to `0.0.0.0:8080` by running:
|
|||||||
libreddit
|
libreddit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Change Default Settings
|
||||||
|
|
||||||
|
Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
||||||
|
|
||||||
|
| Name | Possible values | Default value |
|
||||||
|
|-------------------------|------------------------------------------------------------------------------------------|---------------|
|
||||||
|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"]` | `system` |
|
||||||
|
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||||
|
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||||
|
| `WIDE` | `["on", "off"]` | `off` |
|
||||||
|
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||||
|
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||||
|
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||||
|
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||||
|
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LIBREDDIT_DEFAULT_SHOW_NSFW=on libreddit
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LIBREDDIT_DEFAULT_WIDE=on LIBREDDIT_DEFAULT_THEME=dark libreddit -r
|
||||||
|
```
|
||||||
|
|
||||||
## Proxying using NGINX
|
## Proxying using NGINX
|
||||||
|
|
||||||
**NOTE** If you're [proxying Libreddit through a NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
|
**NOTE** If you're [proxying Libreddit through a NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
|
||||||
@ -202,6 +228,25 @@ proxy_http_version 1.1;
|
|||||||
```
|
```
|
||||||
to your NGINX configuration file above your `proxy_pass` line.
|
to your NGINX configuration file above your `proxy_pass` line.
|
||||||
|
|
||||||
|
## SystemD
|
||||||
|
|
||||||
|
You can use the SystemD service available in `contrib/libreddit.service`
|
||||||
|
(install it on `/etc/systemd/system/libreddit.service`).
|
||||||
|
|
||||||
|
That service can be optionally configured in terms of environment variables by
|
||||||
|
creating a file in `/etc/libreddit.conf`. Use the `contrib/libreddit.conf` as a
|
||||||
|
template. You can also add the `LIBREDDIT_DEFAULT__{X}` settings explained
|
||||||
|
above.
|
||||||
|
|
||||||
|
When "Proxying using NGINX" where the proxy is on the same machine, you should
|
||||||
|
guarantee nginx waits for this service to start. Edit
|
||||||
|
`/etc/systemd/system/libreddit.service.d/reverse-proxy.conf`:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[Unit]
|
||||||
|
Before=nginx.service
|
||||||
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
|
2
contrib/libreddit.conf
Normal file
2
contrib/libreddit.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ADDRESS=localhost
|
||||||
|
PORT=12345
|
15
contrib/libreddit.service
Normal file
15
contrib/libreddit.service
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=libreddit daemon
|
||||||
|
After=network.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
DynamicUser=yes
|
||||||
|
# Default Values
|
||||||
|
Environment=ADDRESS=0.0.0.0
|
||||||
|
Environment=PORT=8080
|
||||||
|
# Optional Override
|
||||||
|
EnvironmentFile=-/etc/libreddit.conf
|
||||||
|
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
@ -9,14 +9,16 @@ use crate::server::RequestExt;
|
|||||||
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
||||||
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
||||||
|
|
||||||
|
// For each parameter in request
|
||||||
for (name, value) in req.params().iter() {
|
for (name, value) in req.params().iter() {
|
||||||
|
// Fill the parameter value in the url
|
||||||
url = url.replace(&format!("{{{}}}", name), value);
|
url = url.replace(&format!("{{{}}}", name), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream(&url).await
|
stream(&url, &req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream(url: &str) -> Result<Response<Body>, String> {
|
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// First parameter is target URL (mandatory).
|
// First parameter is target URL (mandatory).
|
||||||
let url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?;
|
let url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?;
|
||||||
|
|
||||||
@ -26,8 +28,19 @@ async fn stream(url: &str) -> Result<Response<Body>, String> {
|
|||||||
// Build the hyper client from the HTTPS connector.
|
// Build the hyper client from the HTTPS connector.
|
||||||
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
||||||
|
|
||||||
|
let mut builder = Request::get(url);
|
||||||
|
|
||||||
|
// Copy useful headers from original request
|
||||||
|
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
||||||
|
if let Some(value) = req.headers().get(key) {
|
||||||
|
builder = builder.header(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream_request = builder.body(Body::empty()).map_err(|_| "Couldn't build empty body in stream".to_string())?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.get(url)
|
.request(stream_request)
|
||||||
.await
|
.await
|
||||||
.map(|mut res| {
|
.map(|mut res| {
|
||||||
let mut rm = |key: &str| res.headers_mut().remove(key);
|
let mut rm = |key: &str| res.headers_mut().remove(key);
|
||||||
@ -40,19 +53,22 @@ async fn stream(url: &str) -> Result<Response<Body>, String> {
|
|||||||
rm("x-cdn-client-region");
|
rm("x-cdn-client-region");
|
||||||
rm("x-cdn-name");
|
rm("x-cdn-name");
|
||||||
rm("x-cdn-server-region");
|
rm("x-cdn-server-region");
|
||||||
|
rm("x-reddit-cdn");
|
||||||
|
rm("x-reddit-video-features");
|
||||||
|
|
||||||
res
|
res
|
||||||
})
|
})
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
|
fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||||
// Prepare the HTTPS connector.
|
// Prepare the HTTPS connector.
|
||||||
let https = hyper_rustls::HttpsConnector::with_native_roots();
|
let https = hyper_rustls::HttpsConnector::with_native_roots();
|
||||||
|
|
||||||
// Build the hyper client from the HTTPS connector.
|
// Construct the hyper client from the HTTPS connector.
|
||||||
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
||||||
|
|
||||||
|
// Build request
|
||||||
let builder = Request::builder()
|
let builder = Request::builder()
|
||||||
.method("GET")
|
.method("GET")
|
||||||
.uri(&url)
|
.uri(&url)
|
||||||
@ -61,6 +77,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
|
|||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||||
.header("Accept-Language", "en-US,en;q=0.5")
|
.header("Accept-Language", "en-US,en;q=0.5")
|
||||||
.header("Connection", "keep-alive")
|
.header("Connection", "keep-alive")
|
||||||
|
.header("Cookie", if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%7D" } else { "" })
|
||||||
.body(Body::empty());
|
.body(Body::empty());
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
@ -75,6 +92,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
|
|||||||
.map(|val| val.to_str().unwrap_or_default())
|
.map(|val| val.to_str().unwrap_or_default())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
quarantine,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +109,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
|
|||||||
|
|
||||||
// Make a request to a Reddit API and parse the JSON response
|
// Make a request to a Reddit API and parse the JSON response
|
||||||
#[cached(size = 100, time = 30, result = true)]
|
#[cached(size = 100, time = 30, result = true)]
|
||||||
pub async fn json(path: String) -> Result<Value, String> {
|
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||||
// Build Reddit url from path
|
// Build Reddit url from path
|
||||||
let url = format!("https://www.reddit.com{}", path);
|
let url = format!("https://www.reddit.com{}", path);
|
||||||
|
|
||||||
@ -102,7 +120,7 @@ pub async fn json(path: String) -> Result<Value, String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the url...
|
// Fetch the url...
|
||||||
match request(url.clone()).await {
|
match request(url.clone(), quarantine).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// asynchronously aggregate the chunks of the body
|
// asynchronously aggregate the chunks of the body
|
||||||
match hyper::body::aggregate(response).await {
|
match hyper::body::aggregate(response).await {
|
||||||
|
51
src/main.rs
51
src/main.rs
@ -1,14 +1,7 @@
|
|||||||
// Global specifiers
|
// Global specifiers
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(clippy::pedantic, clippy::all)]
|
#![warn(clippy::pedantic, clippy::all)]
|
||||||
#![allow(
|
#![allow(clippy::needless_pass_by_value, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::find_map)]
|
||||||
clippy::needless_pass_by_value,
|
|
||||||
clippy::match_wildcard_for_single_variants,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::similar_names,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::find_map
|
|
||||||
)]
|
|
||||||
|
|
||||||
// Reference local files
|
// Reference local files
|
||||||
mod post;
|
mod post;
|
||||||
@ -66,6 +59,16 @@ async fn favicon() -> Result<Response<Body>, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn font() -> Result<Response<Body>, String> {
|
||||||
|
Ok(
|
||||||
|
Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.header("content-type", "font/woff2")
|
||||||
|
.body(include_bytes!("../static/Inter.var.woff2").as_ref().into())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
|
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
|
||||||
let mut res = Response::builder()
|
let mut res = Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
@ -87,6 +90,13 @@ async fn main() {
|
|||||||
let matches = cli::new("Libreddit")
|
let matches = cli::new("Libreddit")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("Private front-end for Reddit written in Rust ")
|
.about("Private front-end for Reddit written in Rust ")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("redirect-https")
|
||||||
|
.short("r")
|
||||||
|
.long("redirect-https")
|
||||||
|
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
|
||||||
|
.takes_value(false),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("address")
|
Arg::with_name("address")
|
||||||
.short("a")
|
.short("a")
|
||||||
@ -105,13 +115,6 @@ async fn main() {
|
|||||||
.default_value("8080")
|
.default_value("8080")
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name("redirect-https")
|
|
||||||
.short("r")
|
|
||||||
.long("redirect-https")
|
|
||||||
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
|
|
||||||
.takes_value(false),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("hsts")
|
Arg::with_name("hsts")
|
||||||
.short("H")
|
.short("H")
|
||||||
@ -127,7 +130,7 @@ async fn main() {
|
|||||||
let port = matches.value_of("port").unwrap_or("8080");
|
let port = matches.value_of("port").unwrap_or("8080");
|
||||||
let hsts = matches.value_of("hsts");
|
let hsts = matches.value_of("hsts");
|
||||||
|
|
||||||
let listener = format!("{}:{}", address, port);
|
let listener = [address, ":", port].concat();
|
||||||
|
|
||||||
println!("Starting Libreddit...");
|
println!("Starting Libreddit...");
|
||||||
|
|
||||||
@ -139,7 +142,7 @@ async fn main() {
|
|||||||
"Referrer-Policy" => "no-referrer",
|
"Referrer-Policy" => "no-referrer",
|
||||||
"X-Content-Type-Options" => "nosniff",
|
"X-Content-Type-Options" => "nosniff",
|
||||||
"X-Frame-Options" => "DENY",
|
"X-Frame-Options" => "DENY",
|
||||||
"Content-Security-Policy" => "default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';"
|
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;"
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(expire_time) = hsts {
|
if let Some(expire_time) = hsts {
|
||||||
@ -156,11 +159,19 @@ async fn main() {
|
|||||||
app.at("/robots.txt").get(|_| resource("User-agent: *\nAllow: /", "text/plain", true).boxed());
|
app.at("/robots.txt").get(|_| resource("User-agent: *\nAllow: /", "text/plain", true).boxed());
|
||||||
app.at("/favicon.ico").get(|_| favicon().boxed());
|
app.at("/favicon.ico").get(|_| favicon().boxed());
|
||||||
app.at("/logo.png").get(|_| pwa_logo().boxed());
|
app.at("/logo.png").get(|_| pwa_logo().boxed());
|
||||||
|
app.at("/Inter.var.woff2").get(|_| font().boxed());
|
||||||
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
||||||
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
||||||
|
app
|
||||||
|
.at("/playHLSVideo.js")
|
||||||
|
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
|
||||||
|
app
|
||||||
|
.at("/hls.min.js")
|
||||||
|
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
|
||||||
|
|
||||||
// Proxy media through Libreddit
|
// Proxy media through Libreddit
|
||||||
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
|
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
|
||||||
|
app.at("/hls/:id/*path").get(|r| proxy(r, "https://v.redd.it/{id}/{path}").boxed());
|
||||||
app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed());
|
app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed());
|
||||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||||
@ -184,9 +195,13 @@ async fn main() {
|
|||||||
// Configure settings
|
// Configure settings
|
||||||
app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed());
|
app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed());
|
||||||
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
|
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
|
||||||
|
app.at("/settings/update").get(|r| settings::update(r).boxed());
|
||||||
|
|
||||||
// Subreddit services
|
// Subreddit services
|
||||||
app.at("/r/:sub").get(|r| subreddit::community(r).boxed());
|
app
|
||||||
|
.at("/r/:sub")
|
||||||
|
.get(|r| subreddit::community(r).boxed())
|
||||||
|
.post(|r| subreddit::add_quarantine_exception(r).boxed());
|
||||||
|
|
||||||
app
|
app
|
||||||
.at("/r/u_:name")
|
.at("/r/u_:name")
|
||||||
|
182
src/post.rs
182
src/post.rs
@ -2,10 +2,10 @@
|
|||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::esc;
|
use crate::esc;
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::utils::{cookie, error, format_num, format_url, param, rewrite_urls, template, time, val, Author, Comment, Flags, Flair, FlairPart, Media, Post, Preferences};
|
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||||
use hyper::{Body, Request, Response};
|
use crate::utils::{error, format_num, format_url, param, rewrite_urls, setting, template, time, val, Author, Comment, Flags, Flair, FlairPart, Media, Post, Preferences};
|
||||||
|
|
||||||
use async_recursion::async_recursion;
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
@ -23,18 +23,22 @@ struct PostTemplate {
|
|||||||
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
||||||
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
|
|
||||||
// Set sort to sort query parameter
|
// Set sort to sort query parameter
|
||||||
let mut sort: String = param(&path, "sort");
|
let sort = param(&path, "sort").unwrap_or_else(|| {
|
||||||
|
// Grab default comment sort method from Cookies
|
||||||
|
let default_sort = setting(&req, "comment_sort");
|
||||||
|
|
||||||
// Grab default comment sort method from Cookies
|
// If there's no sort query but there's a default sort, set sort to default_sort
|
||||||
let default_sort = cookie(&req, "comment_sort");
|
if default_sort.is_empty() {
|
||||||
|
String::new()
|
||||||
// If there's no sort query but there's a default sort, set sort to default_sort
|
} else {
|
||||||
if sort.is_empty() && !default_sort.is_empty() {
|
path = format!("{}.json?{}&sort={}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), default_sort);
|
||||||
sort = default_sort;
|
default_sort
|
||||||
path = format!("{}.json?{}&sort={}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), sort);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Log the post ID being fetched in debug mode
|
// Log the post ID being fetched in debug mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -44,12 +48,12 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let highlighted_comment = &req.param("comment_id").unwrap_or_default();
|
let highlighted_comment = &req.param("comment_id").unwrap_or_default();
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url, receive JSON in response
|
||||||
match json(path).await {
|
match json(path, quarantined).await {
|
||||||
// Otherwise, grab the JSON output from the request
|
// Otherwise, grab the JSON output from the request
|
||||||
Ok(res) => {
|
Ok(response) => {
|
||||||
// Parse the JSON into Post and Comment structs
|
// Parse the JSON into Post and Comment structs
|
||||||
let post = parse_post(&res[0]).await;
|
let post = parse_post(&response[0]).await;
|
||||||
let comments = parse_comments(&res[1], &post.permalink, &post.author.name, highlighted_comment).await;
|
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment);
|
||||||
|
|
||||||
// Use the Post and Comment structs to generate a website to show users
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
template(PostTemplate {
|
template(PostTemplate {
|
||||||
@ -61,7 +65,14 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
Err(msg) => error(req, msg).await,
|
Err(msg) => {
|
||||||
|
if msg == "quarantined" {
|
||||||
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
|
quarantine(req, sub)
|
||||||
|
} else {
|
||||||
|
error(req, msg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +117,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
media,
|
media,
|
||||||
thumbnail: Media {
|
thumbnail: Media {
|
||||||
url: format_url(val(post, "thumbnail").as_str()),
|
url: format_url(val(post, "thumbnail").as_str()),
|
||||||
|
alt_url: String::new(),
|
||||||
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
|
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||||
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||||
poster: "".to_string(),
|
poster: "".to_string(),
|
||||||
@ -137,79 +149,71 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
#[async_recursion]
|
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
|
||||||
async fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
|
// Parse the comment JSON into a Vector of Comments
|
||||||
// Separate the comment JSON into a Vector of comments
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
let comment_data = match json["data"]["children"].as_array() {
|
|
||||||
Some(f) => f.to_owned(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut comments: Vec<Comment> = Vec::new();
|
|
||||||
|
|
||||||
// For each comment, retrieve the values to build a Comment object
|
// For each comment, retrieve the values to build a Comment object
|
||||||
for comment in comment_data {
|
|
||||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
|
||||||
let data = &comment["data"];
|
|
||||||
|
|
||||||
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
|
||||||
let (rel_time, created) = time(unix_time);
|
|
||||||
|
|
||||||
let edited = match data["edited"].as_f64() {
|
|
||||||
Some(stamp) => time(stamp),
|
|
||||||
None => (String::new(), String::new()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let score = data["score"].as_i64().unwrap_or(0);
|
|
||||||
let body = rewrite_urls(&val(&comment, "body_html"));
|
|
||||||
|
|
||||||
// If this comment contains replies, handle those too
|
|
||||||
let replies: Vec<Comment> = if data["replies"].is_object() {
|
|
||||||
parse_comments(&data["replies"], post_link, post_author, highlighted_comment).await
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent_kind_and_id = val(&comment, "parent_id");
|
|
||||||
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
let id = val(&comment, "id");
|
|
||||||
let highlighted = id == highlighted_comment;
|
|
||||||
|
|
||||||
comments.push(Comment {
|
|
||||||
id,
|
|
||||||
kind,
|
|
||||||
parent_id: parent_info[1].to_string(),
|
|
||||||
parent_kind: parent_info[0].to_string(),
|
|
||||||
post_link: post_link.to_string(),
|
|
||||||
post_author: post_author.to_string(),
|
|
||||||
body,
|
|
||||||
author: Author {
|
|
||||||
name: val(&comment, "author"),
|
|
||||||
flair: Flair {
|
|
||||||
flair_parts: FlairPart::parse(
|
|
||||||
data["author_flair_type"].as_str().unwrap_or_default(),
|
|
||||||
data["author_flair_richtext"].as_array(),
|
|
||||||
data["author_flair_text"].as_str(),
|
|
||||||
),
|
|
||||||
text: esc!(&comment, "link_flair_text"),
|
|
||||||
background_color: val(&comment, "author_flair_background_color"),
|
|
||||||
foreground_color: val(&comment, "author_flair_text_color"),
|
|
||||||
},
|
|
||||||
distinguished: val(&comment, "distinguished"),
|
|
||||||
},
|
|
||||||
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
|
||||||
("\u{2022}".to_string(), "Hidden".to_string())
|
|
||||||
} else {
|
|
||||||
format_num(score)
|
|
||||||
},
|
|
||||||
rel_time,
|
|
||||||
created,
|
|
||||||
edited,
|
|
||||||
replies,
|
|
||||||
highlighted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
comments
|
comments
|
||||||
|
.into_iter()
|
||||||
|
.map(|comment| {
|
||||||
|
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||||
|
let data = &comment["data"];
|
||||||
|
|
||||||
|
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
||||||
|
let (rel_time, created) = time(unix_time);
|
||||||
|
|
||||||
|
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
|
||||||
|
|
||||||
|
let score = data["score"].as_i64().unwrap_or(0);
|
||||||
|
let body = rewrite_urls(&val(&comment, "body_html"));
|
||||||
|
|
||||||
|
// If this comment contains replies, handle those too
|
||||||
|
let replies: Vec<Comment> = if data["replies"].is_object() {
|
||||||
|
parse_comments(&data["replies"], post_link, post_author, highlighted_comment)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent_kind_and_id = val(&comment, "parent_id");
|
||||||
|
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
let id = val(&comment, "id");
|
||||||
|
let highlighted = id == highlighted_comment;
|
||||||
|
|
||||||
|
Comment {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
parent_id: parent_info[1].to_string(),
|
||||||
|
parent_kind: parent_info[0].to_string(),
|
||||||
|
post_link: post_link.to_string(),
|
||||||
|
post_author: post_author.to_string(),
|
||||||
|
body,
|
||||||
|
author: Author {
|
||||||
|
name: val(&comment, "author"),
|
||||||
|
flair: Flair {
|
||||||
|
flair_parts: FlairPart::parse(
|
||||||
|
data["author_flair_type"].as_str().unwrap_or_default(),
|
||||||
|
data["author_flair_richtext"].as_array(),
|
||||||
|
data["author_flair_text"].as_str(),
|
||||||
|
),
|
||||||
|
text: esc!(&comment, "link_flair_text"),
|
||||||
|
background_color: val(&comment, "author_flair_background_color"),
|
||||||
|
foreground_color: val(&comment, "author_flair_text_color"),
|
||||||
|
},
|
||||||
|
distinguished: val(&comment, "distinguished"),
|
||||||
|
},
|
||||||
|
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
||||||
|
("\u{2022}".to_string(), "Hidden".to_string())
|
||||||
|
} else {
|
||||||
|
format_num(score)
|
||||||
|
},
|
||||||
|
rel_time,
|
||||||
|
created,
|
||||||
|
edited,
|
||||||
|
replies,
|
||||||
|
highlighted,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{catch_random, cookie, error, format_num, format_url, param, template, val, Post, Preferences};
|
use crate::utils::{catch_random, error, format_num, format_url, param, setting, template, val, Post, Preferences};
|
||||||
use crate::{client::json, RequestExt};
|
use crate::{
|
||||||
|
client::json,
|
||||||
|
subreddit::{can_access_quarantine, quarantine},
|
||||||
|
RequestExt,
|
||||||
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
@ -31,32 +35,29 @@ struct SearchTemplate {
|
|||||||
sub: String,
|
sub: String,
|
||||||
params: SearchParams,
|
params: SearchParams,
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let nsfw_results = if cookie(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
||||||
let path = format!("{}.json?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
let path = format!("{}.json?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "/find").await {
|
if let Ok(random) = catch_random(&sub, "/find").await {
|
||||||
return Ok(random);
|
return Ok(random);
|
||||||
}
|
}
|
||||||
let query = param(&path, "q");
|
let query = param(&path, "q").unwrap_or_default();
|
||||||
|
|
||||||
let sort = if param(&path, "sort").is_empty() {
|
let sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
|
||||||
"relevance".to_string()
|
|
||||||
} else {
|
|
||||||
param(&path, "sort")
|
|
||||||
};
|
|
||||||
|
|
||||||
let subreddits = if param(&path, "restrict_sr").is_empty() {
|
// If search is not restricted to this subreddit, show other subreddits in search results
|
||||||
search_subreddits(&query).await
|
let subreddits = param(&path, "restrict_sr").map_or(search_subreddits(&query).await, |_| Vec::new());
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
match Post::fetch(&path, String::new()).await {
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
|
match Post::fetch(&path, String::new(), quarantined).await {
|
||||||
Ok((posts, after)) => template(SearchTemplate {
|
Ok((posts, after)) => template(SearchTemplate {
|
||||||
posts,
|
posts,
|
||||||
subreddits,
|
subreddits,
|
||||||
@ -64,14 +65,22 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
params: SearchParams {
|
params: SearchParams {
|
||||||
q: query.replace('"', """),
|
q: query.replace('"', """),
|
||||||
sort,
|
sort,
|
||||||
t: param(&path, "t"),
|
t: param(&path, "t").unwrap_or_default(),
|
||||||
before: param(&path, "after"),
|
before: param(&path, "after").unwrap_or_default(),
|
||||||
after,
|
after,
|
||||||
restrict_sr: param(&path, "restrict_sr"),
|
restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
|
||||||
},
|
},
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
}),
|
}),
|
||||||
Err(msg) => error(req, msg).await,
|
Err(msg) => {
|
||||||
|
if msg == "quarantined" {
|
||||||
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
|
quarantine(req, sub)
|
||||||
|
} else {
|
||||||
|
error(req, msg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,35 +88,25 @@ async fn search_subreddits(q: &str) -> Vec<Subreddit> {
|
|||||||
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
|
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(subreddit_search_path).await {
|
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
|
||||||
// If success, receive JSON in response
|
.as_array()
|
||||||
Ok(response) => {
|
.map(ToOwned::to_owned)
|
||||||
match response["data"]["children"].as_array() {
|
.unwrap_or_default()
|
||||||
// For each subreddit from subreddit list
|
.iter()
|
||||||
Some(list) => list
|
.map(|subreddit| {
|
||||||
.iter()
|
// For each subreddit from subreddit list
|
||||||
.map(|subreddit| {
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
let icon = subreddit["data"]["community_icon"]
|
||||||
let community_icon: &str = subreddit["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
|
.as_str()
|
||||||
let icon = if community_icon.is_empty() {
|
.map_or_else(|| val(&subreddit, "icon_img"), ToString::to_string);
|
||||||
val(&subreddit, "icon_img")
|
|
||||||
} else {
|
|
||||||
community_icon.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Subreddit {
|
Subreddit {
|
||||||
name: val(subreddit, "display_name_prefixed"),
|
name: val(subreddit, "display_name_prefixed"),
|
||||||
url: val(subreddit, "url"),
|
url: val(subreddit, "url"),
|
||||||
icon: format_url(&icon),
|
icon: format_url(&icon),
|
||||||
description: val(subreddit, "public_description"),
|
description: val(subreddit, "public_description"),
|
||||||
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
|
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Subreddit>>(),
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
// If the Reddit API returns an error, exit this function
|
.collect::<Vec<Subreddit>>()
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -69,29 +69,31 @@ impl RequestExt for Request<Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cookies(&self) -> Vec<Cookie> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
let mut cookies = Vec::new();
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
if let Some(header) = self.headers().get("Cookie") {
|
header
|
||||||
for cookie in header.to_str().unwrap_or_default().split("; ") {
|
.to_str()
|
||||||
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")));
|
.unwrap_or_default()
|
||||||
}
|
.split("; ")
|
||||||
}
|
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
|
||||||
cookies
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cookie(&self, name: &str) -> Option<Cookie> {
|
fn cookie(&self, name: &str) -> Option<Cookie> {
|
||||||
self.cookies().iter().find(|c| c.name() == name).map(std::borrow::ToOwned::to_owned)
|
self.cookies().into_iter().find(|c| c.name() == name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseExt for Response<Body> {
|
impl ResponseExt for Response<Body> {
|
||||||
fn cookies(&self) -> Vec<Cookie> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
let mut cookies = Vec::new();
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
for header in self.headers().get_all("Cookie") {
|
header
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.to_str()
|
||||||
cookies.push(cookie);
|
.unwrap_or_default()
|
||||||
}
|
.split("; ")
|
||||||
}
|
.map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
|
||||||
cookies
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_cookie(&mut self, cookie: Cookie) {
|
fn insert_cookie(&mut self, cookie: Cookie) {
|
||||||
@ -144,6 +146,7 @@ impl Server {
|
|||||||
|
|
||||||
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
|
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
|
||||||
let make_svc = make_service_fn(move |_conn| {
|
let make_svc = make_service_fn(move |_conn| {
|
||||||
|
// For correct borrowing, these values need to be borrowed
|
||||||
let router = self.router.clone();
|
let router = self.router.clone();
|
||||||
let default_headers = self.default_headers.clone();
|
let default_headers = self.default_headers.clone();
|
||||||
|
|
||||||
@ -159,7 +162,7 @@ impl Server {
|
|||||||
let mut path = req.uri().path().replace("//", "/");
|
let mut path = req.uri().path().replace("//", "/");
|
||||||
|
|
||||||
// Remove trailing slashes
|
// Remove trailing slashes
|
||||||
if path.ends_with('/') && path != "/" {
|
if path != "/" && path.ends_with('/') {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,17 +201,15 @@ impl Server {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build SocketAddr from provided address
|
||||||
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr));
|
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr));
|
||||||
|
|
||||||
let server = HyperServer::bind(address).serve(make_svc);
|
// Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
|
||||||
|
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
|
||||||
|
// Wait for the CTRL+C signal
|
||||||
|
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler")
|
||||||
|
});
|
||||||
|
|
||||||
let graceful = server.with_graceful_shutdown(shutdown_signal());
|
server.boxed()
|
||||||
|
|
||||||
graceful.boxed()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
|
||||||
// Wait for the CTRL+C signal
|
|
||||||
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler");
|
|
||||||
}
|
|
||||||
|
@ -16,6 +16,20 @@ struct SettingsTemplate {
|
|||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CONSTANTS
|
||||||
|
|
||||||
|
const PREFS: [&str; 9] = [
|
||||||
|
"theme",
|
||||||
|
"front_page",
|
||||||
|
"layout",
|
||||||
|
"wide",
|
||||||
|
"comment_sort",
|
||||||
|
"post_sort",
|
||||||
|
"show_nsfw",
|
||||||
|
"use_hls",
|
||||||
|
"hide_hls_notification",
|
||||||
|
];
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
|
|
||||||
// Retrieve cookies from request "Cookie" header
|
// Retrieve cookies from request "Cookie" header
|
||||||
@ -29,12 +43,12 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let (parts, mut body) = req.into_parts();
|
let (parts, mut body) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let mut cookies = Vec::new();
|
let _cookies: Vec<Cookie> = parts
|
||||||
for header in parts.headers.get_all("Cookie") {
|
.headers
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.get_all("Cookie")
|
||||||
cookies.push(cookie);
|
.iter()
|
||||||
}
|
.filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
|
||||||
}
|
.collect();
|
||||||
|
|
||||||
// Aggregate the body...
|
// Aggregate the body...
|
||||||
// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
|
// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
|
||||||
@ -48,64 +62,72 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
|
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let mut res = redirect("/settings".to_string());
|
let mut response = redirect("/settings".to_string());
|
||||||
|
|
||||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw"];
|
for &name in &PREFS {
|
||||||
|
|
||||||
for name in names {
|
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => res.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value.to_owned())
|
Cookie::build(name.to_owned(), value.to_owned())
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
.finish(),
|
.finish(),
|
||||||
),
|
),
|
||||||
None => res.remove_cookie(name.to_string()),
|
None => response.remove_cookie(name.to_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(res)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cookies using response "Set-Cookie" header
|
fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body> {
|
||||||
pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
|
|
||||||
// Split the body into parts
|
// Split the body into parts
|
||||||
let (parts, _) = req.into_parts();
|
let (parts, _) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let mut cookies = Vec::new();
|
let _cookies: Vec<Cookie> = parts
|
||||||
for header in parts.headers.get_all("Cookie") {
|
.headers
|
||||||
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) {
|
.get_all("Cookie")
|
||||||
cookies.push(cookie);
|
.iter()
|
||||||
}
|
.filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
|
||||||
}
|
.collect();
|
||||||
|
|
||||||
let query = parts.uri.query().unwrap_or_default().as_bytes();
|
let query = parts.uri.query().unwrap_or_default().as_bytes();
|
||||||
|
|
||||||
let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
|
let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"];
|
|
||||||
|
|
||||||
let path = match form.get("redirect") {
|
let path = match form.get("redirect") {
|
||||||
Some(value) => format!("/{}/", value),
|
Some(value) => format!("/{}", value.replace("%26", "&").replace("%23", "#")),
|
||||||
None => "/".to_string(),
|
None => "/".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = redirect(path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
for name in names {
|
for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => res.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value.to_owned())
|
Cookie::build(name.to_owned(), value.to_owned())
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
.finish(),
|
.finish(),
|
||||||
),
|
),
|
||||||
None => res.remove_cookie(name.to_string()),
|
None => {
|
||||||
|
if remove_cookies {
|
||||||
|
response.remove_cookie(name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(res)
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookies using response "Set-Cookie" header
|
||||||
|
pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
|
Ok(set_cookies_method(req, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
|
Ok(set_cookies_method(req, false))
|
||||||
}
|
}
|
||||||
|
205
src/subreddit.rs
205
src/subreddit.rs
@ -1,6 +1,6 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::esc;
|
use crate::esc;
|
||||||
use crate::utils::{catch_random, cookie, error, format_num, format_url, param, redirect, rewrite_urls, template, val, Post, Preferences, Subreddit};
|
use crate::utils::{catch_random, error, format_num, format_url, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit};
|
||||||
use crate::{client::json, server::ResponseExt, RequestExt};
|
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
@ -16,6 +16,7 @@ struct SubredditTemplate {
|
|||||||
sort: (String, String),
|
sort: (String, String),
|
||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -27,11 +28,22 @@ struct WikiTemplate {
|
|||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "wall.html", escape = "none")]
|
||||||
|
struct WallTemplate {
|
||||||
|
title: String,
|
||||||
|
sub: String,
|
||||||
|
msg: String,
|
||||||
|
prefs: Preferences,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let subscribed = cookie(&req, "subscriptions");
|
let root = req.uri().path() == "/";
|
||||||
let front_page = cookie(&req, "front_page");
|
let subscribed = setting(&req, "subscriptions");
|
||||||
|
let front_page = setting(&req, "front_page");
|
||||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
||||||
|
|
||||||
@ -44,6 +56,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
} else {
|
} else {
|
||||||
front_page.to_owned()
|
front_page.to_owned()
|
||||||
});
|
});
|
||||||
|
let quarantined = can_access_quarantine(&req, &sub) || root;
|
||||||
|
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "").await {
|
if let Ok(random) = catch_random(&sub, "").await {
|
||||||
@ -56,16 +69,16 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
||||||
|
|
||||||
match Post::fetch(&path, String::new()).await {
|
match Post::fetch(&path, String::new(), quarantined).await {
|
||||||
Ok((posts, after)) => {
|
Ok((posts, after)) => {
|
||||||
// If you can get subreddit posts, also request subreddit metadata
|
// If you can get subreddit posts, also request subreddit metadata
|
||||||
let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" {
|
let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" {
|
||||||
// Regular subreddit
|
// Regular subreddit
|
||||||
subreddit(&sub).await.unwrap_or_default()
|
subreddit(&sub, quarantined).await.unwrap_or_default()
|
||||||
} else if sub == subscribed {
|
} else if sub == subscribed {
|
||||||
// Subscription feed
|
// Subscription feed
|
||||||
if req.uri().path().starts_with("/r/") {
|
if req.uri().path().starts_with("/r/") {
|
||||||
subreddit(&sub).await.unwrap_or_default()
|
subreddit(&sub, quarantined).await.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
}
|
}
|
||||||
@ -79,16 +92,19 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
template(SubredditTemplate {
|
template(SubredditTemplate {
|
||||||
sub,
|
sub,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t")),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after"), after),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(msg) => match msg.as_str() {
|
Err(msg) => match msg.as_str() {
|
||||||
"quarantined" => error(req, format!("r/{} has been quarantined by Reddit", sub)).await,
|
"quarantined" => quarantine(req, sub),
|
||||||
"private" => error(req, format!("r/{} is a private community", sub)).await,
|
"private" => error(req, format!("r/{} is a private community", sub)).await,
|
||||||
"banned" => error(req, format!("r/{} has been banned from Reddit", sub)).await,
|
"banned" => error(req, format!("r/{} has been banned from Reddit", sub)).await,
|
||||||
_ => error(req, msg).await,
|
_ => error(req, msg).await,
|
||||||
@ -96,6 +112,43 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, String> {
|
||||||
|
let wall = WallTemplate {
|
||||||
|
title: format!("r/{} is quarantined", sub),
|
||||||
|
msg: "Please click the button below to continue to this subreddit.".to_string(),
|
||||||
|
url: req.uri().to_string(),
|
||||||
|
sub,
|
||||||
|
prefs: Preferences::new(req),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
Response::builder()
|
||||||
|
.status(403)
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.body(wall.render().unwrap_or_default().into())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
|
let subreddit = req.param("sub").ok_or("Invalid URL")?;
|
||||||
|
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
|
||||||
|
let mut response = redirect(redir);
|
||||||
|
response.insert_cookie(
|
||||||
|
Cookie::build(&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.expires(cookie::Expiration::Session)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
|
||||||
|
// Determine if the subreddit can be accessed
|
||||||
|
setting(&req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
||||||
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
@ -110,16 +163,19 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
let mut sub_list = Preferences::new(req).subscriptions;
|
let mut sub_list = Preferences::new(req).subscriptions;
|
||||||
|
|
||||||
// Retrieve list of posts for these subreddits to extract display names
|
// Retrieve list of posts for these subreddits to extract display names
|
||||||
let display = json(format!("/r/{}/hot.json?raw_json=1", sub)).await?;
|
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await?;
|
||||||
let display_lookup: Vec<(String, &str)> = display["data"]["children"]
|
let display_lookup: Vec<(String, &str)> = posts["data"]["children"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.map(|list| {
|
||||||
.iter()
|
list
|
||||||
.map(|post| {
|
.iter()
|
||||||
let display_name = post["data"]["subreddit"].as_str().unwrap();
|
.map(|post| {
|
||||||
(display_name.to_lowercase(), display_name)
|
let display_name = post["data"]["subreddit"].as_str().unwrap_or_default();
|
||||||
|
(display_name.to_lowercase(), display_name)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Find each subreddit name (separated by '+') in sub parameter
|
// Find each subreddit name (separated by '+') in sub parameter
|
||||||
for part in sub.split('+') {
|
for part in sub.split('+') {
|
||||||
@ -131,7 +187,7 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
} else {
|
} else {
|
||||||
// This subreddit display name isn't known, retrieve it
|
// This subreddit display name isn't known, retrieve it
|
||||||
let path: String = format!("/r/{}/about.json?raw_json=1", part);
|
let path: String = format!("/r/{}/about.json?raw_json=1", part);
|
||||||
display = json(path).await?;
|
display = json(path, true).await?;
|
||||||
display["data"]["display_name"].as_str().ok_or_else(|| "Failed to query subreddit name".to_string())?
|
display["data"]["display_name"].as_str().ok_or_else(|| "Failed to query subreddit name".to_string())?
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,26 +199,25 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
sub_list.sort_by_key(|a| a.to_lowercase())
|
sub_list.sort_by_key(|a| a.to_lowercase())
|
||||||
} else if action.contains(&"unsubscribe".to_string()) {
|
} else if action.contains(&"unsubscribe".to_string()) {
|
||||||
// Remove sub name from subscribed list
|
// Remove sub name from subscribed list
|
||||||
sub_list.retain(|s| s != part);
|
sub_list.retain(|s| s.to_lowercase() != part.to_lowercase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to subreddit
|
// Redirect back to subreddit
|
||||||
// check for redirect parameter if unsubscribing from outside sidebar
|
// check for redirect parameter if unsubscribing from outside sidebar
|
||||||
let redirect_path = param(&format!("/?{}", query), "redirect");
|
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
||||||
let path = if redirect_path.is_empty() {
|
|
||||||
format!("/r/{}", sub)
|
|
||||||
} else {
|
|
||||||
format!("/{}/", redirect_path)
|
format!("/{}/", redirect_path)
|
||||||
|
} else {
|
||||||
|
format!("/r/{}", sub)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = redirect(path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
// Delete cookie if empty, else set
|
// Delete cookie if empty, else set
|
||||||
if sub_list.is_empty() {
|
if sub_list.is_empty() {
|
||||||
res.remove_cookie("subscriptions".to_string());
|
response.remove_cookie("subscriptions".to_string());
|
||||||
} else {
|
} else {
|
||||||
res.insert_cookie(
|
response.insert_cookie(
|
||||||
Cookie::build("subscriptions", sub_list.join("+"))
|
Cookie::build("subscriptions", sub_list.join("+"))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
@ -171,11 +226,12 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(res)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||||
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "/wiki").await {
|
if let Ok(random) = catch_random(&sub, "/wiki").await {
|
||||||
return Ok(random);
|
return Ok(random);
|
||||||
@ -184,19 +240,27 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
||||||
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||||
|
|
||||||
match json(path).await {
|
match json(path, quarantined).await {
|
||||||
Ok(response) => template(WikiTemplate {
|
Ok(response) => template(WikiTemplate {
|
||||||
sub,
|
sub,
|
||||||
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
|
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
|
||||||
page,
|
page,
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
}),
|
}),
|
||||||
Err(msg) => error(req, msg).await,
|
Err(msg) => {
|
||||||
|
if msg == "quarantined" {
|
||||||
|
quarantine(req, sub)
|
||||||
|
} else {
|
||||||
|
error(req, msg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||||
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
|
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
|
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
|
||||||
return Ok(random);
|
return Ok(random);
|
||||||
@ -206,26 +270,32 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path).await {
|
match json(path, quarantined).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => template(WikiTemplate {
|
Ok(response) => template(WikiTemplate {
|
||||||
wiki: format!(
|
wiki: format!(
|
||||||
"{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
"{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
||||||
rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
||||||
moderators(&sub).await?.join(""),
|
moderators(&sub, quarantined).await?.join(""),
|
||||||
),
|
),
|
||||||
sub,
|
sub,
|
||||||
page: "Sidebar".to_string(),
|
page: "Sidebar".to_string(),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
}),
|
}),
|
||||||
Err(msg) => error(req, msg).await,
|
Err(msg) => {
|
||||||
|
if msg == "quarantined" {
|
||||||
|
quarantine(req, sub)
|
||||||
|
} else {
|
||||||
|
error(req, msg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn moderators(sub: &str) -> Result<Vec<String>, String> {
|
pub async fn moderators(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
|
||||||
// Retrieve and format the html for the moderators list
|
// Retrieve and format the html for the moderators list
|
||||||
Ok(
|
Ok(
|
||||||
moderators_list(sub)
|
moderators_list(sub, quarantined)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
|
.map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
|
||||||
@ -233,57 +303,54 @@ pub async fn moderators(sub: &str) -> Result<Vec<String>, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn moderators_list(sub: &str) -> Result<Vec<String>, String> {
|
async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
|
||||||
// Build the moderator list URL
|
// Build the moderator list URL
|
||||||
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
|
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Retrieve response
|
// Retrieve response
|
||||||
let response = json(path).await?["data"]["children"].clone();
|
json(path, quarantined).await.map(|response| {
|
||||||
Ok(if let Some(response) = response.as_array() {
|
|
||||||
// Traverse json tree and format into list of strings
|
// Traverse json tree and format into list of strings
|
||||||
response
|
response["data"]["children"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap_or(&Vec::new())
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| m["name"].as_str().unwrap_or(""))
|
.filter_map(|moderator| {
|
||||||
.filter(|m| !m.is_empty())
|
let name = moderator["name"].as_str().unwrap_or_default();
|
||||||
.map(std::string::ToString::to_string)
|
if name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUBREDDIT
|
// SUBREDDIT
|
||||||
async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API url
|
||||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path).await {
|
let res = json(path, quarantined).await?;
|
||||||
// If success, receive JSON in response
|
|
||||||
Ok(res) => {
|
|
||||||
// Metadata regarding the subreddit
|
|
||||||
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
|
||||||
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
|
||||||
|
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
// Metadata regarding the subreddit
|
||||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap();
|
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
||||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
||||||
|
|
||||||
let sub = Subreddit {
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
name: esc!(&res, "display_name"),
|
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
|
||||||
title: esc!(&res, "title"),
|
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||||
description: esc!(&res, "public_description"),
|
|
||||||
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
|
|
||||||
moderators: moderators_list(sub).await?,
|
|
||||||
icon: format_url(&icon),
|
|
||||||
members: format_num(members),
|
|
||||||
active: format_num(active),
|
|
||||||
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sub)
|
Ok(Subreddit {
|
||||||
}
|
name: esc!(&res, "display_name"),
|
||||||
// If the Reddit API returns an error, exit this function
|
title: esc!(&res, "title"),
|
||||||
Err(msg) => return Err(msg),
|
description: esc!(&res, "public_description"),
|
||||||
}
|
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
|
||||||
|
moderators: moderators_list(sub, quarantined).await?,
|
||||||
|
icon: format_url(&icon),
|
||||||
|
members: format_num(members),
|
||||||
|
active: format_num(active),
|
||||||
|
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
46
src/user.rs
46
src/user.rs
@ -16,6 +16,7 @@ struct UserTemplate {
|
|||||||
sort: (String, String),
|
sort: (String, String),
|
||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
@ -28,11 +29,12 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Retrieve other variables from Libreddit request
|
// Retrieve other variables from Libreddit request
|
||||||
let sort = param(&path, "sort");
|
let sort = param(&path, "sort").unwrap_or_default();
|
||||||
let username = req.param("name").unwrap_or_default();
|
let username = req.param("name").unwrap_or_default();
|
||||||
|
|
||||||
// Request user posts/comments from Reddit
|
// Request user posts/comments from Reddit
|
||||||
let posts = Post::fetch(&path, "Comment".to_string()).await;
|
let posts = Post::fetch(&path, "Comment".to_string(), false).await;
|
||||||
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
match posts {
|
match posts {
|
||||||
Ok((posts, after)) => {
|
Ok((posts, after)) => {
|
||||||
@ -42,9 +44,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
template(UserTemplate {
|
template(UserTemplate {
|
||||||
user,
|
user,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t")),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after"), after),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// If there is an error show error page
|
// If there is an error show error page
|
||||||
@ -58,27 +61,22 @@ async fn user(name: &str) -> Result<User, String> {
|
|||||||
let path: String = format!("/user/{}/about.json?raw_json=1", name);
|
let path: String = format!("/user/{}/about.json?raw_json=1", name);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path).await {
|
json(path, false).await.map(|res| {
|
||||||
// If success, receive JSON in response
|
// Grab creation date as unix timestamp
|
||||||
Ok(res) => {
|
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||||
// Grab creation date as unix timestamp
|
|
||||||
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
|
||||||
|
|
||||||
// Closure used to parse JSON from Reddit APIs
|
// Closure used to parse JSON from Reddit APIs
|
||||||
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
// Parse the JSON output into a User struct
|
// Parse the JSON output into a User struct
|
||||||
Ok(User {
|
User {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
title: esc!(about("title")),
|
title: esc!(about("title")),
|
||||||
icon: format_url(&about("icon_img")),
|
icon: format_url(&about("icon_img")),
|
||||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||||
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
||||||
banner: esc!(about("banner_img")),
|
banner: esc!(about("banner_img")),
|
||||||
description: about("public_description"),
|
description: about("public_description"),
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit this function
|
})
|
||||||
Err(msg) => return Err(msg),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
197
src/utils.rs
197
src/utils.rs
@ -75,6 +75,7 @@ pub struct Flags {
|
|||||||
|
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub alt_url: String,
|
||||||
pub width: i64,
|
pub width: i64,
|
||||||
pub height: i64,
|
pub height: i64,
|
||||||
pub poster: String,
|
pub poster: String,
|
||||||
@ -85,12 +86,28 @@ impl Media {
|
|||||||
let mut gallery = Vec::new();
|
let mut gallery = Vec::new();
|
||||||
|
|
||||||
// If post is a video, return the video
|
// If post is a video, return the video
|
||||||
let (post_type, url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() {
|
let (post_type, url_val, alt_url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() {
|
||||||
// Return reddit video
|
// Return reddit video
|
||||||
("video", &data["preview"]["reddit_video_preview"]["fallback_url"])
|
(
|
||||||
|
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
|
||||||
|
"gif"
|
||||||
|
} else {
|
||||||
|
"video"
|
||||||
|
},
|
||||||
|
&data["preview"]["reddit_video_preview"]["fallback_url"],
|
||||||
|
Some(&data["preview"]["reddit_video_preview"]["hls_url"]),
|
||||||
|
)
|
||||||
} else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() {
|
} else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() {
|
||||||
// Return reddit video
|
// Return reddit video
|
||||||
("video", &data["secure_media"]["reddit_video"]["fallback_url"])
|
(
|
||||||
|
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
|
||||||
|
"gif"
|
||||||
|
} else {
|
||||||
|
"video"
|
||||||
|
},
|
||||||
|
&data["secure_media"]["reddit_video"]["fallback_url"],
|
||||||
|
Some(&data["secure_media"]["reddit_video"]["hls_url"]),
|
||||||
|
)
|
||||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||||
// Handle images, whether GIFs or pics
|
// Handle images, whether GIFs or pics
|
||||||
let preview = &data["preview"]["images"][0];
|
let preview = &data["preview"]["images"][0];
|
||||||
@ -98,26 +115,26 @@ impl Media {
|
|||||||
|
|
||||||
if mp4.is_object() {
|
if mp4.is_object() {
|
||||||
// Return the mp4 if the media is a gif
|
// Return the mp4 if the media is a gif
|
||||||
("gif", &mp4["source"]["url"])
|
("gif", &mp4["source"]["url"], None)
|
||||||
} else {
|
} else {
|
||||||
// Return the picture if the media is an image
|
// Return the picture if the media is an image
|
||||||
if data["domain"] == "i.redd.it" {
|
if data["domain"] == "i.redd.it" {
|
||||||
("image", &data["url"])
|
("image", &data["url"], None)
|
||||||
} else {
|
} else {
|
||||||
("image", &preview["source"]["url"])
|
("image", &preview["source"]["url"], None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if data["is_self"].as_bool().unwrap_or_default() {
|
} else if data["is_self"].as_bool().unwrap_or_default() {
|
||||||
// If type is self, return permalink
|
// If type is self, return permalink
|
||||||
("self", &data["permalink"])
|
("self", &data["permalink"], None)
|
||||||
} else if data["is_gallery"].as_bool().unwrap_or_default() {
|
} else if data["is_gallery"].as_bool().unwrap_or_default() {
|
||||||
// If this post contains a gallery of images
|
// If this post contains a gallery of images
|
||||||
gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]);
|
gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]);
|
||||||
|
|
||||||
("gallery", &data["url"])
|
("gallery", &data["url"], None)
|
||||||
} else {
|
} else {
|
||||||
// If type can't be determined, return url
|
// If type can't be determined, return url
|
||||||
("link", &data["url"])
|
("link", &data["url"], None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let source = &data["preview"]["images"][0]["source"];
|
let source = &data["preview"]["images"][0]["source"];
|
||||||
@ -128,10 +145,13 @@ impl Media {
|
|||||||
format_url(url_val.as_str().unwrap_or_default())
|
format_url(url_val.as_str().unwrap_or_default())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
||||||
|
|
||||||
(
|
(
|
||||||
post_type.to_string(),
|
post_type.to_string(),
|
||||||
Self {
|
Self {
|
||||||
url,
|
url,
|
||||||
|
alt_url,
|
||||||
width: source["width"].as_i64().unwrap_or_default(),
|
width: source["width"].as_i64().unwrap_or_default(),
|
||||||
height: source["height"].as_i64().unwrap_or_default(),
|
height: source["height"].as_i64().unwrap_or_default(),
|
||||||
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
||||||
@ -197,12 +217,12 @@ pub struct Post {
|
|||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
||||||
pub async fn fetch(path: &str, fallback_title: String) -> Result<(Vec<Self>, String), String> {
|
pub async fn fetch(path: &str, fallback_title: String, quarantine: bool) -> Result<(Vec<Self>, String), String> {
|
||||||
let res;
|
let res;
|
||||||
let post_list;
|
let post_list;
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path.to_string()).await {
|
match json(path.to_string(), quarantine).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
res = response;
|
res = response;
|
||||||
@ -259,6 +279,7 @@ impl Post {
|
|||||||
post_type,
|
post_type,
|
||||||
thumbnail: Media {
|
thumbnail: Media {
|
||||||
url: format_url(val(post, "thumbnail").as_str()),
|
url: format_url(val(post, "thumbnail").as_str()),
|
||||||
|
alt_url: String::new(),
|
||||||
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||||
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||||
poster: "".to_string(),
|
poster: "".to_string(),
|
||||||
@ -365,6 +386,8 @@ pub struct Preferences {
|
|||||||
pub layout: String,
|
pub layout: String,
|
||||||
pub wide: String,
|
pub wide: String,
|
||||||
pub show_nsfw: String,
|
pub show_nsfw: String,
|
||||||
|
pub hide_hls_notification: String,
|
||||||
|
pub use_hls: String,
|
||||||
pub comment_sort: String,
|
pub comment_sort: String,
|
||||||
pub post_sort: String,
|
pub post_sort: String,
|
||||||
pub subscriptions: Vec<String>,
|
pub subscriptions: Vec<String>,
|
||||||
@ -374,14 +397,16 @@ impl Preferences {
|
|||||||
// Build preferences from cookies
|
// Build preferences from cookies
|
||||||
pub fn new(req: Request<Body>) -> Self {
|
pub fn new(req: Request<Body>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
theme: cookie(&req, "theme"),
|
theme: setting(&req, "theme"),
|
||||||
front_page: cookie(&req, "front_page"),
|
front_page: setting(&req, "front_page"),
|
||||||
layout: cookie(&req, "layout"),
|
layout: setting(&req, "layout"),
|
||||||
wide: cookie(&req, "wide"),
|
wide: setting(&req, "wide"),
|
||||||
show_nsfw: cookie(&req, "show_nsfw"),
|
show_nsfw: setting(&req, "show_nsfw"),
|
||||||
comment_sort: cookie(&req, "comment_sort"),
|
use_hls: setting(&req, "use_hls"),
|
||||||
post_sort: cookie(&req, "post_sort"),
|
hide_hls_notification: setting(&req, "hide_hls_notification"),
|
||||||
subscriptions: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
comment_sort: setting(&req, "comment_sort"),
|
||||||
|
post_sort: setting(&req, "post_sort"),
|
||||||
|
subscriptions: setting(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,29 +416,45 @@ impl Preferences {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Grab a query parameter from a url
|
// Grab a query parameter from a url
|
||||||
pub fn param(path: &str, value: &str) -> String {
|
pub fn param(path: &str, value: &str) -> Option<String> {
|
||||||
match Url::parse(format!("https://libredd.it/{}", path).as_str()) {
|
Some(
|
||||||
Ok(url) => url.query_pairs().into_owned().collect::<HashMap<_, _>>().get(value).unwrap_or(&String::new()).to_owned(),
|
Url::parse(format!("https://libredd.it/{}", path).as_str())
|
||||||
_ => String::new(),
|
.ok()?
|
||||||
}
|
.query_pairs()
|
||||||
|
.into_owned()
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
.get(value)?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a cookie value from request
|
// Retrieve the value of a setting by name
|
||||||
pub fn cookie(req: &Request<Body>, name: &str) -> String {
|
pub fn setting(req: &Request<Body>, name: &str) -> String {
|
||||||
let cookie = req.cookie(name).unwrap_or_else(|| Cookie::named(name));
|
// Parse a cookie value from request
|
||||||
cookie.value().to_string()
|
req
|
||||||
|
.cookie(name)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
// If there is no cookie for this setting, try receiving a default from an environment variable
|
||||||
|
if let Ok(default) = std::env::var(format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) {
|
||||||
|
Cookie::new(name, default)
|
||||||
|
} else {
|
||||||
|
Cookie::named(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.value()
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect and redirect in the event of a random subreddit
|
// Detect and redirect in the event of a random subreddit
|
||||||
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
||||||
if (sub == "random" || sub == "randnsfw") && !sub.contains('+') {
|
if (sub == "random" || sub == "randnsfw") && !sub.contains('+') {
|
||||||
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub)).await?["data"]["display_name"]
|
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
return Ok(redirect(format!("/r/{}{}", new_sub, additional)));
|
Ok(redirect(format!("/r/{}{}", new_sub, additional)))
|
||||||
} else {
|
} else {
|
||||||
return Err("No redirect needed".to_string());
|
Err("No redirect needed".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,59 +463,71 @@ pub fn format_url(url: &str) -> String {
|
|||||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
match Url::parse(url) {
|
Url::parse(url).map_or(String::new(), |parsed| {
|
||||||
Ok(parsed) => {
|
let domain = parsed.domain().unwrap_or_default();
|
||||||
let domain = parsed.domain().unwrap_or_default();
|
|
||||||
|
|
||||||
let capture = |regex: &str, format: &str, segments: i16| {
|
let capture = |regex: &str, format: &str, segments: i16| {
|
||||||
Regex::new(regex)
|
Regex::new(regex).map_or(String::new(), |re| {
|
||||||
.map(|re| match re.captures(url) {
|
re.captures(url).map_or(String::new(), |caps| match segments {
|
||||||
Some(caps) => match segments {
|
1 => [format, &caps[1]].join(""),
|
||||||
1 => [format, &caps[1]].join(""),
|
2 => [format, &caps[1], "/", &caps[2]].join(""),
|
||||||
2 => [format, &caps[1], "/", &caps[2]].join(""),
|
_ => String::new(),
|
||||||
_ => String::new(),
|
})
|
||||||
},
|
})
|
||||||
None => String::new(),
|
};
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
macro_rules! chain {
|
||||||
|
() => {
|
||||||
|
{
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match domain {
|
( $first_fn:expr, $($other_fns:expr), *) => {
|
||||||
"v.redd.it" => capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
|
{
|
||||||
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
|
let result = $first_fn;
|
||||||
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
|
if result.is_empty() {
|
||||||
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
|
chain!($($other_fns,)*)
|
||||||
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
|
}
|
||||||
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1),
|
else
|
||||||
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
|
{
|
||||||
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
|
result
|
||||||
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
|
}
|
||||||
_ => String::new(),
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Err(_) => String::new(),
|
|
||||||
}
|
match domain {
|
||||||
|
"v.redd.it" => chain!(
|
||||||
|
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
|
||||||
|
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
|
||||||
|
),
|
||||||
|
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
|
||||||
|
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
|
||||||
|
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
|
||||||
|
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
|
||||||
|
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1),
|
||||||
|
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
|
||||||
|
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
|
||||||
|
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite Reddit links to Libreddit in body of text
|
// Rewrite Reddit links to Libreddit in body of text
|
||||||
pub fn rewrite_urls(input_text: &str) -> String {
|
pub fn rewrite_urls(input_text: &str) -> String {
|
||||||
let text1 = match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
|
let text1 = Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#).map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string());
|
||||||
Ok(re) => re.replace_all(input_text, r#"href="/"#).to_string(),
|
|
||||||
Err(_) => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rewrite external media previews to Libreddit
|
// Rewrite external media previews to Libreddit
|
||||||
match Regex::new(r"https://external-preview\.redd\.it(.*)[^?]") {
|
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
|
||||||
Ok(re) => {
|
if re.is_match(&text1) {
|
||||||
if re.is_match(&text1) {
|
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
|
||||||
re.replace_all(&text1, format_url(re.find(&text1).unwrap().as_str())).to_string()
|
} else {
|
||||||
} else {
|
text1
|
||||||
text1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => String::new(),
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append `m` and `k` for millions and thousands respectively
|
// Append `m` and `k` for millions and thousands respectively
|
||||||
|
BIN
static/Inter.var.woff2
Normal file
BIN
static/Inter.var.woff2
Normal file
Binary file not shown.
5
static/hls.min.js
vendored
Normal file
5
static/hls.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
77
static/playHLSVideo.js
Normal file
77
static/playHLSVideo.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
||||||
|
(function () {
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
||||||
|
videoSources.forEach(function (source) {
|
||||||
|
var playlist = source.src;
|
||||||
|
|
||||||
|
var oldVideo = source.parentNode;
|
||||||
|
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
||||||
|
|
||||||
|
// If HLS is supported natively then don't use hls.js
|
||||||
|
if (oldVideo.canPlayType(source.type)) {
|
||||||
|
if (autoplay) {
|
||||||
|
oldVideo.play();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace video with copy that will have all "source" elements removed
|
||||||
|
var newVideo = oldVideo.cloneNode(true);
|
||||||
|
var allSources = newVideo.querySelectorAll("source");
|
||||||
|
allSources.forEach(function (source) {
|
||||||
|
source.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty source to enable play event
|
||||||
|
newVideo.src = "about:blank";
|
||||||
|
|
||||||
|
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
||||||
|
|
||||||
|
function initializeHls() {
|
||||||
|
newVideo.removeEventListener('play', initializeHls);
|
||||||
|
|
||||||
|
var hls = new Hls({ autoStartLoad: false });
|
||||||
|
hls.loadSource(playlist);
|
||||||
|
hls.attachMedia(newVideo);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||||
|
hls.loadLevel = hls.levels.length - 1;
|
||||||
|
hls.startLoad();
|
||||||
|
newVideo.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
var errorType = data.type;
|
||||||
|
var errorFatal = data.fatal;
|
||||||
|
if (errorFatal) {
|
||||||
|
switch (errorType) {
|
||||||
|
case Hls.ErrorType.NETWORK_ERROR:
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorType.MEDIA_ERROR:
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("HLS error", data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newVideo.addEventListener('play', initializeHls);
|
||||||
|
|
||||||
|
if (autoplay) {
|
||||||
|
newVideo.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var videos = document.querySelectorAll("video.hls_autoplay");
|
||||||
|
videos.forEach(function (video) {
|
||||||
|
video.setAttribute("autoplay", "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// @license-end
|
@ -6,6 +6,12 @@
|
|||||||
--admin: #ea0027;
|
--admin: #ea0027;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('/Inter.var.woff2') format('woff2-variations');
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* Automatic theme selection */
|
/* Automatic theme selection */
|
||||||
:root, .dark{
|
:root, .dark{
|
||||||
/* Default & fallback theme (dark) */
|
/* Default & fallback theme (dark) */
|
||||||
@ -150,7 +156,7 @@ html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
|
|||||||
pre, form, fieldset, table, th, td, select, input {
|
pre, form, fieldset, table, th, td, select, input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -664,6 +670,7 @@ a.search_subreddit:hover {
|
|||||||
"post_score post_title post_thumbnail" 1fr
|
"post_score post_title post_thumbnail" 1fr
|
||||||
"post_score post_media post_thumbnail" auto
|
"post_score post_media post_thumbnail" auto
|
||||||
"post_score post_body post_thumbnail" auto
|
"post_score post_body post_thumbnail" auto
|
||||||
|
"post_score post_notification post_thumbnail" auto
|
||||||
"post_score post_footer post_thumbnail" auto
|
"post_score post_footer post_thumbnail" auto
|
||||||
/ minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px));
|
/ minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px));
|
||||||
}
|
}
|
||||||
@ -706,6 +713,17 @@ a.search_subreddit:hover {
|
|||||||
grid-area: post_title;
|
grid-area: post_title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post_notification {
|
||||||
|
grid-area: post_notification;
|
||||||
|
margin: 5px 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_notification a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.post_flair {
|
.post_flair {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
@ -950,6 +968,7 @@ a.search_subreddit:hover {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_body.highlighted {
|
.comment_body.highlighted {
|
||||||
@ -1177,7 +1196,7 @@ input[type="submit"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md code {
|
.md code {
|
||||||
font-family: monospace;
|
font-family: monospace, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1256,6 +1275,7 @@ td, th {
|
|||||||
"post_title post_title post_thumbnail" 1fr
|
"post_title post_title post_thumbnail" 1fr
|
||||||
"post_media post_media post_thumbnail" auto
|
"post_media post_media post_thumbnail" auto
|
||||||
"post_body post_body post_thumbnail" auto
|
"post_body post_body post_thumbnail" auto
|
||||||
|
"post_notification post_notification post_thumbnail" auto
|
||||||
"post_score post_footer post_thumbnail" auto
|
"post_score post_footer post_thumbnail" auto
|
||||||
/ auto 1fr fit-content(min(20%, 152px));
|
/ auto 1fr fit-content(min(20%, 152px));
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,17 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
||||||
|
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||||
|
<script src="/hls.min.js"></script>
|
||||||
|
<video class="post_media_video short hls_autoplay" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" controls preload="none">
|
||||||
|
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||||
|
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
<script src="/playHLSVideo.js"></script>
|
||||||
|
{% else %}
|
||||||
<video class="post_media_video" src="{{ post.media.url }}" controls autoplay loop><a href={{ post.media.url }}>Video</a></video>
|
<video class="post_media_video" src="{{ post.media.url }}" controls autoplay loop><a href={{ post.media.url }}>Video</a></video>
|
||||||
|
{% call utils::render_hls_notification(post.permalink[1..]) %}
|
||||||
|
{% endif %}
|
||||||
{% else if post.post_type == "gallery" %}
|
{% else if post.post_type == "gallery" %}
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
{% for image in post.gallery -%}
|
{% for image in post.gallery -%}
|
||||||
@ -77,14 +87,14 @@
|
|||||||
<figcaption>
|
<figcaption>
|
||||||
<p>{{ image.caption }}</p>
|
<p>{{ image.caption }}</p>
|
||||||
{% if image.outbound_url.len() > 0 %}
|
{% if image.outbound_url.len() > 0 %}
|
||||||
<p><a class="outbound_url" href="{{ image.outbound_url }}">{{ image.outbound_url }}</a>
|
<p><a class="outbound_url" href="{{ image.outbound_url }}" rel="nofollow">{{ image.outbound_url }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else if post.post_type == "link" %}
|
{% else if post.post_type == "link" %}
|
||||||
<a id="post_url" href="{{ post.media.url }}">{{ post.media.url }}</a>
|
<a id="post_url" href="{{ post.media.url }}" rel="nofollow">{{ post.media.url }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- POST BODY -->
|
<!-- POST BODY -->
|
||||||
@ -93,7 +103,7 @@
|
|||||||
<div class="post_footer">
|
<div class="post_footer">
|
||||||
<ul id="post_links">
|
<ul id="post_links">
|
||||||
<li><a href="/{{ post.id }}">permalink</a></li>
|
<li><a href="/{{ post.id }}">permalink</a></li>
|
||||||
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li>
|
<li><a href="https://reddit.com/{{ post.id }}" rel="nofollow">reddit</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>{{ post.upvote_ratio }}% Upvoted</p>
|
<p>{{ post.upvote_ratio }}% Upvoted</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="wide">
|
<div id="wide">
|
||||||
<label for="wide">Wide UI:</label>
|
<label for="wide">Wide UI:</label>
|
||||||
|
<input type="hidden" value="off" name="wide">
|
||||||
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<p>Content</p>
|
<p>Content</p>
|
||||||
@ -50,8 +51,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="show_nsfw">
|
<div id="show_nsfw">
|
||||||
<label for="show_nsfw">Show NSFW posts:</label>
|
<label for="show_nsfw">Show NSFW posts:</label>
|
||||||
|
<input type="hidden" value="off" name="show_nsfw">
|
||||||
<input type="checkbox" name="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}>
|
<input type="checkbox" name="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="use_hls">
|
||||||
|
<label for="use_hls">Use HLS for videos</label>
|
||||||
|
<input type="hidden" value="off" name="use_hls">
|
||||||
|
<input type="checkbox" name="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div id="hide_hls_notification">
|
||||||
|
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
|
||||||
|
<input type="hidden" value="off" name="hide_hls_notification">
|
||||||
|
<input type="checkbox" name="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
<input id="save" type="submit" value="Save">
|
<input id="save" type="submit" value="Save">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -71,7 +83,7 @@
|
|||||||
|
|
||||||
<div id="settings_note">
|
<div id="settings_note">
|
||||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&subscriptions={{ prefs.subscriptions.join("%2B") }}">this link</a>.</p>
|
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&subscriptions={{ prefs.subscriptions.join("%2B") }}">this link</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@
|
|||||||
{% call utils::post_in_list(post) %}
|
{% call utils::post_in_list(post) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if prefs.use_hls == "on" %}
|
||||||
|
<script src="/hls.min.js"></script>
|
||||||
|
<script src="/playHLSVideo.js"></script>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
@ -55,8 +55,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_hls_notification(redirect_url) -%}
|
||||||
|
{% if post.post_type == "video" && !post.media.alt_url.is_empty() && prefs.hide_hls_notification != "on" %}
|
||||||
|
<div class="post_notification"><p><a href="/settings/update/?use_hls=on&redirect={{ redirect_url }}">Enable HLS</a> to view with audio, or <a href="/settings/update/?hide_hls_notification=on&redirect={{ redirect_url }}">disable this notification</a></p></div>
|
||||||
|
{% endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro post_in_list(post) -%}
|
{% macro post_in_list(post) -%}
|
||||||
<div class="post {% if post.flags.stickied %}stickied{% endif %}">
|
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
|
||||||
<p class="post_header">
|
<p class="post_header">
|
||||||
{% let community -%}
|
{% let community -%}
|
||||||
{% if post.community.starts_with("u_") -%}
|
{% if post.community.starts_with("u_") -%}
|
||||||
@ -95,9 +101,17 @@
|
|||||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
|
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
|
||||||
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" controls loop autoplay><a href={{ post.media.url }}>Video</a></video>
|
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" controls loop autoplay><a href={{ post.media.url }}>Video</a></video>
|
||||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
||||||
|
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||||
|
<video class="post_media_video short" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" controls preload="none">
|
||||||
|
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||||
|
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
{% else %}
|
||||||
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls autoplay><a href={{ post.media.url }}>Video</a></video>
|
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls autoplay><a href={{ post.media.url }}>Video</a></video>
|
||||||
|
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26"), post.id)) %}
|
||||||
|
{% endif %}
|
||||||
{% else if post.post_type != "self" %}
|
{% else if post.post_type != "self" %}
|
||||||
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}">
|
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||||
{% if post.thumbnail.url.is_empty() %}
|
{% if post.thumbnail.url.is_empty() %}
|
||||||
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title>Thumbnail</title>
|
<title>Thumbnail</title>
|
||||||
|
13
templates/wall.html
Normal file
13
templates/wall.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ msg }}{% endblock %}
|
||||||
|
{% block sortstyle %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="wall">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<br>
|
||||||
|
<p>{{ msg }}</p>
|
||||||
|
<form action="/r/{{ sub }}?redir={{ url }}" method="POST">
|
||||||
|
<input id="save" type="submit" value="Continue">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user