Compare commits
72 Commits
Author | SHA1 | Date | |
---|---|---|---|
9c1a932214 | |||
8c0269af1c | |||
df89c5076e | |||
f819ad2bc6 | |||
f5884a5270 | |||
c046d00060 | |||
5934e34ea0 | |||
463b44ac52 | |||
b40d21e559 | |||
a422a74747 | |||
4124fa87d3 | |||
1dd0c4ee20 | |||
0dd114c166 | |||
67090e9b08 | |||
d97fb49fde | |||
9263b0657f | |||
a3384cbaa6 | |||
5d26b5c764 | |||
516403ee47 | |||
5ea504e6e8 | |||
f49bff9853 | |||
4ec529cdb8 | |||
779de6f8af | |||
0925a9b334 | |||
2f2ed6169d | |||
59ef30c76d | |||
d43b49e7e4 | |||
64a92195dd | |||
a7925ed62d | |||
39ba50dada | |||
bc1b29246d | |||
2d77a91150 | |||
93c1db502d | |||
a6dc7ee043 | |||
c7282520cd | |||
a866c1d068 | |||
aa9aad6743 | |||
f65ee2eb6a | |||
44c4341e67 | |||
1c886f8003 | |||
b481d26be2 | |||
f00ef59404 | |||
3115ff3436 | |||
443b198c12 | |||
ac84d8d2db | |||
e27cf94fbf | |||
68495fb280 | |||
bec5c78709 | |||
abfcfdf09e | |||
dad01749e6 | |||
2efb73cee3 | |||
ace21b21d5 | |||
280e16bd7f | |||
44d44a529c | |||
0957f2e339 | |||
3516404a5f | |||
d96daa335f | |||
285d9da26d | |||
9ab7a72bce | |||
46dd905509 | |||
63d595c67d | |||
dc0b5f42e6 | |||
9ecbd25488 | |||
83816fbcc6 | |||
11cfbdc3ed | |||
4b7cbb3de2 | |||
b1a572072c | |||
b1071e9579 | |||
da971f8680 | |||
b596f86cc2 | |||
3bcf0832a1 | |||
565f4f23b3 |
158
Cargo.lock
generated
158
Cargo.lock
generated
@ -92,8 +92,8 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
|
||||
dependencies = [
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -266,15 +266,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.14.0"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
||||
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@ -345,8 +345,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -356,8 +356,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -561,8 +561,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -603,8 +603,8 @@ checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -695,8 +695,8 @@ checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -753,22 +753,13 @@ dependencies = [
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
@ -807,9 +798,9 @@ checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@ -972,9 +963,9 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@ -1015,17 +1006,18 @@ checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
|
||||
|
||||
[[package]]
|
||||
name = "libreddit"
|
||||
version = "0.2.1"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"askama",
|
||||
"async-recursion",
|
||||
"base64 0.13.0",
|
||||
"chrono",
|
||||
"pulldown-cmark",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1228,9 +1220,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0"
|
||||
checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
@ -1271,8 +1263,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1282,8 +1274,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1340,18 +1332,6 @@ dependencies = [
|
||||
"unicode-xid 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@ -1369,9 +1349,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
]
|
||||
@ -1583,15 +1563,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.60"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
|
||||
checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1631,9 +1611,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab"
|
||||
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -1646,15 +1626,15 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
||||
checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.3.18"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97e0e9fd577458a4f61fb91fcb559ea2afecc54c934119421f9f5d3d5b1a1057"
|
||||
checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
@ -1669,9 +1649,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "standback"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8"
|
||||
checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0"
|
||||
dependencies = [
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
@ -1697,10 +1677,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"quote 1.0.8",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn 1.0.54",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1711,12 +1691,12 @@ checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
|
||||
dependencies = [
|
||||
"base-x",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"quote 1.0.8",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"syn 1.0.54",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1738,33 +1718,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.54"
|
||||
version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
|
||||
checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"quote 1.0.8",
|
||||
"unicode-xid 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e"
|
||||
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
|
||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1829,9 +1809,9 @@ checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"quote 1.0.8",
|
||||
"standback",
|
||||
"syn 1.0.54",
|
||||
"syn 1.0.57",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2027,12 +2007,6 @@ version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.1.0"
|
||||
@ -2119,8 +2093,8 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -2142,7 +2116,7 @@ version = "0.2.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
|
||||
dependencies = [
|
||||
"quote 1.0.7",
|
||||
"quote 1.0.8",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
@ -2153,8 +2127,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
"quote 1.0.8",
|
||||
"syn 1.0.57",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
15
Cargo.toml
15
Cargo.toml
@ -3,21 +3,18 @@ name = "libreddit"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/spikecodes/libreddit"
|
||||
version = "0.2.1"
|
||||
version = "0.2.6"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["proxy"]
|
||||
proxy = ["actix-web/rustls", "base64"]
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.13.0", optional = true }
|
||||
actix-web = "3.2.0"
|
||||
base64 = "0.13.0"
|
||||
actix-web = { version = "3.2.0", features = ["rustls"] }
|
||||
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
|
||||
askama = "0.8.0"
|
||||
serde = "1.0.117"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.8.0"
|
||||
chrono = "0.4.19"
|
||||
async-recursion = "0.3.1"
|
||||
async-recursion = "0.3.1"
|
||||
url = "2.2.0"
|
||||
regex = "1"
|
139
README.md
139
README.md
@ -2,19 +2,54 @@
|
||||
|
||||
> An alternative private front-end to Reddit
|
||||
|
||||
Libre + Reddit = Libreddit
|
||||
Libre + Reddit = [Libreddit](https://libredd.it)
|
||||
|
||||
- 🚀 Fast: written in Rust for blazing fast speeds and safety
|
||||
- ☁️ Light: no javascript, no ads, no tracking
|
||||
- ☁️ Light: no JavaScript, no ads, no tracking
|
||||
- 🕵 Private: all requests are proxied through the server, including media
|
||||
- 🔒 Safe: does not rely on Reddit OAuth or require a Reddit API Key
|
||||
- 📱 Responsive: works great on mobile!
|
||||
- 🦺 Safe: does not rely on Reddit OAuth or require a Reddit API Key
|
||||
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
|
||||
|
||||
Think Invidious but for Reddit. Watch your cat videos without being watched.
|
||||
Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libredd.it/r/unpopularopinion) without being [tracked](#reddit).
|
||||
|
||||
## Contents
|
||||
- [Screenshot](#screenshot)
|
||||
- [Instances](#instances)
|
||||
- [About](#about)
|
||||
- [Elsewhere](#elsewhere)
|
||||
- [Info](#info)
|
||||
- [Teddit Comparison](#how-does-it-compare-to-teddit)
|
||||
- [Comparison](#comparison)
|
||||
- [Speed](#speed)
|
||||
- [Privacy](#privacy)
|
||||
- [Installation](#installation)
|
||||
- [Cargo](#a-cargo)
|
||||
- [Docker](#b-docker)
|
||||
- [AUR](#c-aur)
|
||||
- [GitHub Releases](#d-github-releases)
|
||||
- [Repl.it](#e-replit)
|
||||
- Developing
|
||||
- [Deployment](#deployment)
|
||||
- [Building](#building)
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Instances
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here!
|
||||
|
||||
| Website | Country | Cloudflare |
|
||||
|-|-|-|
|
||||
| [libredd.it](https://libredd.it) (official) | 🇺🇸 US | |
|
||||
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
|
||||
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.insanity.wtf](https://libreddit.insanity.wtf) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ |
|
||||
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||
|
||||
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||
|
||||
## About
|
||||
|
||||
@ -26,13 +61,9 @@ Find Libreddit on...
|
||||
- 🦊 GitLab: [spikecodes/libreddit](https://gitlab.com/spikecodes/libreddit)
|
||||
|
||||
### Info
|
||||
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||
|
||||
Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below.
|
||||
|
||||
### In Progress
|
||||
- Searching
|
||||
- Multireddits
|
||||
Libreddit currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/spikecodes/libreddit/issues).
|
||||
|
||||
### How does it compare to Teddit?
|
||||
|
||||
@ -40,15 +71,66 @@ Teddit is another awesome open source project designed to provide an alternative
|
||||
|
||||
If you are looking to compare, the biggest differences I have noticed are:
|
||||
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||
- Libreddit is written in Rust for speed and memory safety. It uses Actix Web, which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
||||
- Unlike Teddit (at the time of writing this), Libreddit does not require a Reddit API key to host.
|
||||
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
||||
|
||||
## Instances
|
||||
## Comparison
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your selfhosted instance listed here!
|
||||
This section outlines how Libreddit compares to Reddit.
|
||||
|
||||
- [libredd.it](https://libredd.it) 🇺🇸 (Thank you to [YeapGuy](https://github.com/YeapGuy)!)
|
||||
- [libreddit.spike.codes](https://libreddit.spike.codes) 🇺🇸
|
||||
### Speed
|
||||
|
||||
Lasted tested December 21, 2020.
|
||||
|
||||
Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdotdevsite.appspot.com/lh/html?url=https%3A%2F%2Flibredd.it), [Reddit Report](https://lighthouse-dot-webdotdevsite.appspot.com/lh/html?url=https%3A%2F%2Fwww.reddit.com%2F)).
|
||||
|
||||
| | Libreddit | Reddit |
|
||||
|---------------------|---------------|-----------|
|
||||
| Requests | 22 | 70 |
|
||||
| Resource Size | 135 KiB | 2,222 KiB |
|
||||
| Time to Interactive | **1.7 s** | **11.5 s**|
|
||||
|
||||
### Privacy
|
||||
|
||||
#### Reddit
|
||||
|
||||
**Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including:
|
||||
- IP address
|
||||
- User-agent string
|
||||
- Browser type
|
||||
- Operating system
|
||||
- Referral URLs
|
||||
- Device information (e.g., device IDs)
|
||||
- Device settings
|
||||
- Pages visited
|
||||
- Links clicked
|
||||
- The requested URL
|
||||
- Search terms
|
||||
|
||||
**Location:** The same privacy policy goes on to describe location data may be collected through the use of:
|
||||
- GPS (consensual)
|
||||
- Bluetooth (consensual)
|
||||
- Content associated with a location (consensual)
|
||||
- Your IP Address
|
||||
|
||||
**Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding:
|
||||
- Authentication
|
||||
- Functionality
|
||||
- Analytics and Performance
|
||||
- Advertising
|
||||
- Third-Party Cookies
|
||||
- Third-Party Site
|
||||
|
||||
#### Libreddit
|
||||
|
||||
For transparency, I hope to describe all the ways Libreddit handles user privacy.
|
||||
|
||||
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs and URL paths fetched to aid troubleshooting but nothing else.
|
||||
|
||||
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
|
||||
|
||||
**Cookies:** Libreddit uses no cookies currently but eventually, I plan to add a configuration page where users can store an optional cookie to save their preferred theme, default sorting algorithm, or default layout.
|
||||
|
||||
**Hosting:** The official instances (`libredd.it` and `libreddit.spike.codes`) are hosted on [Repl.it](https://repl.it/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting and browsing through Tor are welcomed.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -84,9 +166,19 @@ yay -S libreddit-git
|
||||
### D) GitHub Releases
|
||||
|
||||
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/spikecodes/libreddit/releases/latest).
|
||||
Currently Libreddit does not have Windows or MacOS binaries but those will be available soon.
|
||||
Currently, Libreddit does not have Windows or macOS binaries but those will be available soon.
|
||||
|
||||
## Deploy an Instance
|
||||
### E) Repl.it
|
||||
|
||||
**Note:** Repl.it is a free option but they are *not* private and are monitor server usage to prevent abuse. If you really need a free and easy setup, this method may work best for you.
|
||||
|
||||
1. Create a Repl.it account (see note above)
|
||||
2. Visit [the official Repl](https://repl.it/@spikethecoder/libreddit) and fork it
|
||||
3. Hit the run button to download the latest Libreddit version and start it
|
||||
|
||||
In the web preview (defaults to top right), you should see your instance hosted where you can assign a [custom domain](https://docs.repl.it/repls/web-hosting#custom-domains).
|
||||
|
||||
## Deployment
|
||||
|
||||
Once installed, deploy Libreddit (unless you're using Docker) by running:
|
||||
|
||||
@ -99,12 +191,7 @@ Specify a custom address for the server by passing the `-a` or `--address` argum
|
||||
libreddit --address=0.0.0.0:8111
|
||||
```
|
||||
|
||||
To disable the media proxy built into Libreddit, run:
|
||||
```
|
||||
libreddit --no-default-features
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
## Building
|
||||
|
||||
```
|
||||
git clone https://github.com/spikecodes/libreddit
|
||||
|
39
src/main.rs
39
src/main.rs
@ -2,9 +2,10 @@
|
||||
use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServer};
|
||||
|
||||
// Reference local files
|
||||
mod popular;
|
||||
mod post;
|
||||
mod proxy;
|
||||
mod search;
|
||||
// mod settings;
|
||||
mod subreddit;
|
||||
mod user;
|
||||
mod utils;
|
||||
@ -31,38 +32,54 @@ async fn main() -> std::io::Result<()> {
|
||||
if args.len() > 1 {
|
||||
for arg in args {
|
||||
if arg.starts_with("--address=") || arg.starts_with("-a=") {
|
||||
let split: Vec<&str> = arg.split("=").collect();
|
||||
let split: Vec<&str> = arg.split('=').collect();
|
||||
address = split[1].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start http server
|
||||
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), address.clone());
|
||||
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
// TRAILING SLASH MIDDLEWARE
|
||||
.wrap(NormalizePath::default())
|
||||
// DEFAULT SERVICE
|
||||
.default_service(web::get().to(utils::error))
|
||||
// GENERAL SERVICES
|
||||
.route("/style.css/", web::get().to(style))
|
||||
.route("/favicon.ico/", web::get().to(|| HttpResponse::Ok()))
|
||||
.route("/favicon.ico/", web::get().to(HttpResponse::Ok))
|
||||
.route("/robots.txt/", web::get().to(robots))
|
||||
// SETTINGS SERVICE
|
||||
// .route("/settings/", web::get().to(settings::get))
|
||||
// .route("/settings/save/", web::post().to(settings::set))
|
||||
// PROXY SERVICE
|
||||
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
|
||||
// SEARCH SERVICES
|
||||
.route("/search/", web::get().to(search::find))
|
||||
.route("r/{sub}/search/", web::get().to(search::find))
|
||||
// USER SERVICES
|
||||
.route("/u/{username}/", web::get().to(user::page))
|
||||
.route("/user/{username}/", web::get().to(user::page))
|
||||
.route("/u/{username}/", web::get().to(user::profile))
|
||||
.route("/user/{username}/", web::get().to(user::profile))
|
||||
// WIKI SERVICES
|
||||
.route("/wiki/", web::get().to(subreddit::wiki))
|
||||
.route("/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
|
||||
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||
// SUBREDDIT SERVICES
|
||||
.route("/r/{sub}/", web::get().to(subreddit::page))
|
||||
.route("/r/{sub}/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||
// POPULAR SERVICES
|
||||
.route("/", web::get().to(popular::page))
|
||||
.route("/", web::get().to(subreddit::page))
|
||||
.route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||
// POST SERVICES
|
||||
.route("/{id:.{5,6}}/", web::get().to(post::short))
|
||||
.route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::page))
|
||||
.route("/{id:.{5,6}}/", web::get().to(post::item))
|
||||
.route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::item))
|
||||
.route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::item))
|
||||
})
|
||||
.bind(address.clone())
|
||||
.expect(format!("Cannot bind to the address: {}", address).as_str())
|
||||
.bind(&address)
|
||||
.unwrap_or_else(|_| panic!("Cannot bind to the address: {}", address))
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
// CRATES
|
||||
use crate::utils::{fetch_posts, ErrorTemplate, Params, Post};
|
||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "popular.html", escape = "none")]
|
||||
struct PopularTemplate {
|
||||
posts: Vec<Post>,
|
||||
sort: String,
|
||||
ends: (String, String),
|
||||
}
|
||||
|
||||
// RENDER
|
||||
async fn render(sub_name: String, sort: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
|
||||
let sorting = sort.unwrap_or("hot".to_string());
|
||||
let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let url = match ends.0 {
|
||||
Some(val) => format!("https://www.reddit.com/r/{}/{}.json?before={}&count=25", sub_name, sorting, val),
|
||||
None => match ends.1 {
|
||||
Some(val) => format!("https://www.reddit.com/r/{}/{}.json?after={}&count=25", sub_name, sorting, val),
|
||||
None => format!("https://www.reddit.com/r/{}/{}.json", sub_name, sorting),
|
||||
},
|
||||
};
|
||||
|
||||
let items_result = fetch_posts(url, String::new()).await;
|
||||
|
||||
if items_result.is_err() {
|
||||
let s = ErrorTemplate {
|
||||
message: items_result.err().unwrap().to_string(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let items = items_result.unwrap();
|
||||
|
||||
let s = PopularTemplate {
|
||||
posts: items.0,
|
||||
sort: sorting,
|
||||
ends: (before, items.1),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
}
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn page(params: web::Query<Params>) -> Result<HttpResponse> {
|
||||
render("popular".to_string(), params.sort.clone(), (params.before.clone(), params.after.clone())).await
|
||||
}
|
163
src/post.rs
163
src/post.rs
@ -1,12 +1,11 @@
|
||||
// CRATES
|
||||
use crate::utils::{format_num, format_url, request, val, Comment, ErrorTemplate, Flair, Params, Post};
|
||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
||||
use crate::utils::{error, format_num, format_url, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -17,54 +16,29 @@ struct PostTemplate {
|
||||
sort: String,
|
||||
}
|
||||
|
||||
async fn render(id: String, sort: Option<String>) -> Result<HttpResponse> {
|
||||
// Log the post ID being fetched
|
||||
dbg!(&id);
|
||||
pub async fn item(req: HttpRequest) -> HttpResponse {
|
||||
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||
let sort = param(&path, "sort");
|
||||
|
||||
// Handling sort paramater
|
||||
let sorting: String = sort.unwrap_or("confidence".to_string());
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let url: String = format!("https://reddit.com/{}.json?sort={}", id, sorting);
|
||||
// Log the post ID being fetched in debug mode
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(req.match_info().get("id").unwrap_or(""));
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
let req = request(url).await;
|
||||
match request(&path).await {
|
||||
// Otherwise, grab the JSON output from the request
|
||||
Ok(res) => {
|
||||
// Parse the JSON into Post and Comment structs
|
||||
let post = parse_post(&res[0]).await.unwrap();
|
||||
let comments = parse_comments(&res[1]).await.unwrap();
|
||||
|
||||
// If the Reddit API returns an error, exit and send error page to user
|
||||
if req.is_err() {
|
||||
let s = ErrorTemplate {
|
||||
message: req.err().unwrap().to_string(),
|
||||
// Use the Post and Comment structs to generate a website to show users
|
||||
let s = PostTemplate { comments, post, sort }.render().unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
return Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s));
|
||||
// If the Reddit API returns an error, exit and send error page to user
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
}
|
||||
|
||||
// Otherwise, grab the JSON output from the request
|
||||
let res = req.unwrap();
|
||||
|
||||
// Parse the JSON into Post and Comment structs
|
||||
let post = parse_post(res[0].clone()).await;
|
||||
let comments = parse_comments(res[1].clone()).await;
|
||||
|
||||
// Use the Post and Comment structs to generate a website to show users
|
||||
let s = PostTemplate {
|
||||
comments: comments.unwrap(),
|
||||
post: post.unwrap(),
|
||||
sort: sorting,
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn short(web::Path(id): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
||||
render(id, params.sort.clone()).await
|
||||
}
|
||||
|
||||
pub async fn page(web::Path((_sub, id)): web::Path<(String, String)>, params: web::Query<Params>) -> Result<HttpResponse> {
|
||||
render(id, params.sort.clone()).await
|
||||
}
|
||||
|
||||
// UTILITIES
|
||||
@ -72,107 +46,104 @@ async fn media(data: &serde_json::Value) -> (String, String) {
|
||||
let post_type: &str;
|
||||
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap()).await
|
||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default().to_string())
|
||||
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap()).await
|
||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default().to_string())
|
||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||
post_type = "image";
|
||||
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap()).await
|
||||
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap_or_default().to_string())
|
||||
} else {
|
||||
post_type = "link";
|
||||
data["url"].as_str().unwrap().to_string()
|
||||
data["url"].as_str().unwrap_or_default().to_string()
|
||||
};
|
||||
|
||||
(post_type.to_string(), url)
|
||||
}
|
||||
|
||||
async fn markdown_to_html(md: &str) -> String {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
options.insert(Options::ENABLE_TASKLISTS);
|
||||
let parser = Parser::new_ext(md, options);
|
||||
|
||||
// Write to String buffer.
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
html_output
|
||||
}
|
||||
|
||||
// POSTS
|
||||
async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
|
||||
let post_data: &serde_json::Value = &json["data"]["children"][0];
|
||||
async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
|
||||
// Retrieve post (as opposed to comments) from JSON
|
||||
let post: &serde_json::Value = &json["data"]["children"][0];
|
||||
|
||||
let unix_time: i64 = post_data["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||
let score = post_data["data"]["score"].as_i64().unwrap();
|
||||
// Grab UTC time as unix timestamp
|
||||
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
|
||||
// Parse post score and upvote ratio
|
||||
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
|
||||
let media = media(&post_data["data"]).await;
|
||||
// Determine the type of media along with the media URL
|
||||
let media = media(&post["data"]).await;
|
||||
|
||||
let post = Post {
|
||||
title: val(post_data, "title").await,
|
||||
community: val(post_data, "subreddit").await,
|
||||
body: markdown_to_html(post_data["data"]["selftext"].as_str().unwrap()).await,
|
||||
author: val(post_data, "author").await,
|
||||
// Build a post using data parsed from Reddit post API
|
||||
Ok(Post {
|
||||
id: val(post, "id"),
|
||||
title: val(post, "title"),
|
||||
community: val(post, "subreddit"),
|
||||
body: rewrite_url(&val(post, "selftext_html")),
|
||||
author: val(post, "author"),
|
||||
author_flair: Flair(
|
||||
val(post_data, "author_flair_text").await,
|
||||
val(post_data, "author_flair_background_color").await,
|
||||
val(post_data, "author_flair_text_color").await,
|
||||
val(post, "author_flair_text"),
|
||||
val(post, "author_flair_background_color"),
|
||||
val(post, "author_flair_text_color"),
|
||||
),
|
||||
url: val(post_data, "permalink").await,
|
||||
permalink: val(post, "permalink"),
|
||||
score: format_num(score),
|
||||
upvote_ratio: ratio as i64,
|
||||
post_type: media.0,
|
||||
media: media.1,
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
||||
flair: Flair(
|
||||
val(post_data, "link_flair_text").await,
|
||||
val(post_data, "link_flair_background_color").await,
|
||||
if val(post_data, "link_flair_text_color").await == "dark" {
|
||||
val(post, "link_flair_text"),
|
||||
val(post, "link_flair_background_color"),
|
||||
if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
} else {
|
||||
"white".to_string()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
Ok(post)
|
||||
),
|
||||
flags: Flags {
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
|
||||
},
|
||||
media: media.1,
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// COMMENTS
|
||||
#[async_recursion]
|
||||
async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'static str> {
|
||||
async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'static str> {
|
||||
// Separate the comment JSON into a Vector of comments
|
||||
let comment_data = json["data"]["children"].as_array().unwrap();
|
||||
|
||||
let mut comments: Vec<Comment> = Vec::new();
|
||||
|
||||
// For each comment, retrieve the values to build a Comment object
|
||||
for comment in comment_data.iter() {
|
||||
for comment in comment_data {
|
||||
let unix_time: i64 = comment["data"]["created_utc"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
if unix_time == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let score = comment["data"]["score"].as_i64().unwrap_or(0);
|
||||
let body = markdown_to_html(comment["data"]["body"].as_str().unwrap_or("")).await;
|
||||
let body = rewrite_url(&val(comment, "body_html"));
|
||||
|
||||
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
|
||||
parse_comments(comment["data"]["replies"].clone()).await.unwrap_or(Vec::new())
|
||||
parse_comments(&comment["data"]["replies"]).await.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
comments.push(Comment {
|
||||
body: body,
|
||||
author: val(comment, "author").await,
|
||||
id: val(comment, "id"),
|
||||
body,
|
||||
author: val(comment, "author"),
|
||||
score: format_num(score),
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
||||
replies: replies,
|
||||
replies,
|
||||
flair: Flair(
|
||||
val(comment, "author_flair_text").await,
|
||||
val(comment, "author_flair_background_color").await,
|
||||
val(comment, "author_flair_text_color").await,
|
||||
val(comment, "author_flair_text"),
|
||||
val(comment, "author_flair_background_color"),
|
||||
val(comment, "author_flair_text_color"),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
59
src/proxy.rs
59
src/proxy.rs
@ -1,29 +1,46 @@
|
||||
use actix_web::{client::Client, web, Error, HttpResponse, Result};
|
||||
use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use base64::decode;
|
||||
|
||||
pub async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> {
|
||||
if cfg!(feature = "proxy") {
|
||||
let media: String;
|
||||
pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
|
||||
let domains = vec![
|
||||
// THUMBNAILS
|
||||
"a.thumbs.redditmedia.com",
|
||||
"b.thumbs.redditmedia.com",
|
||||
// ICONS
|
||||
"styles.redditmedia.com",
|
||||
"www.redditstatic.com",
|
||||
// PREVIEWS
|
||||
"preview.redd.it",
|
||||
"external-preview.redd.it",
|
||||
// MEDIA
|
||||
"i.redd.it",
|
||||
"v.redd.it",
|
||||
];
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
let media = url;
|
||||
match decode(b64) {
|
||||
Ok(bytes) => {
|
||||
let media = String::from_utf8(bytes).unwrap_or_default();
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
match decode(url) {
|
||||
Ok(bytes) => media = String::from_utf8(bytes).unwrap(),
|
||||
Err(_e) => return Ok(HttpResponse::Ok().body("")),
|
||||
};
|
||||
match Url::parse(media.as_str()) {
|
||||
Ok(url) => {
|
||||
let domain = url.domain().unwrap_or_default();
|
||||
|
||||
let client = Client::default();
|
||||
client
|
||||
.get(media.replace("&", "&"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.and_then(|res| Ok(HttpResponse::build(res.status()).streaming(res)))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
if domains.contains(&domain) {
|
||||
Client::default()
|
||||
.get(media.replace("&", "&"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.map(|res| HttpResponse::build(res.status()).streaming(res))
|
||||
} else {
|
||||
Err(error::ErrorForbidden("Resource must be from Reddit"))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(error::ErrorBadRequest("Can't parse encoded base64 URL")),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(error::ErrorBadRequest("Can't decode base64 URL")),
|
||||
}
|
||||
}
|
||||
|
53
src/search.rs
Normal file
53
src/search.rs
Normal file
@ -0,0 +1,53 @@
|
||||
// CRATES
|
||||
use crate::utils::{error, fetch_posts, param, Post};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
// STRUCTS
|
||||
struct SearchParams {
|
||||
q: String,
|
||||
sort: String,
|
||||
t: String,
|
||||
before: String,
|
||||
after: String,
|
||||
restrict_sr: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "search.html", escape = "none")]
|
||||
struct SearchTemplate {
|
||||
posts: Vec<Post>,
|
||||
sub: String,
|
||||
params: SearchParams,
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn find(req: HttpRequest) -> HttpResponse {
|
||||
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||
let sort = if param(&path, "sort").is_empty() {
|
||||
"relevance".to_string()
|
||||
} else {
|
||||
param(&path, "sort")
|
||||
};
|
||||
let sub = req.match_info().get("sub").unwrap_or("").to_string();
|
||||
|
||||
match fetch_posts(&path, String::new()).await {
|
||||
Ok(posts) => HttpResponse::Ok().content_type("text/html").body(
|
||||
SearchTemplate {
|
||||
posts: posts.0,
|
||||
sub,
|
||||
params: SearchParams {
|
||||
q: param(&path, "q"),
|
||||
sort,
|
||||
t: param(&path, "t"),
|
||||
before: param(&path, "after"),
|
||||
after: posts.1,
|
||||
restrict_sr: param(&path, "restrict_sr"),
|
||||
},
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
),
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
}
|
||||
}
|
48
src/settings.rs
Normal file
48
src/settings.rs
Normal file
@ -0,0 +1,48 @@
|
||||
// // CRATES
|
||||
// use crate::utils::cookies;
|
||||
// use actix_web::{cookie::Cookie, web::Form, HttpRequest, HttpResponse, Result}; // http::Method,
|
||||
// use askama::Template;
|
||||
|
||||
// // STRUCTS
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "settings.html", escape = "none")]
|
||||
// struct SettingsTemplate {
|
||||
// pref_nsfw: String,
|
||||
// }
|
||||
|
||||
// #[derive(serde::Deserialize)]
|
||||
// pub struct Preferences {
|
||||
// pref_nsfw: Option<String>,
|
||||
// }
|
||||
|
||||
// // FUNCTIONS
|
||||
|
||||
// // Retrieve cookies from request "Cookie" header
|
||||
// pub async fn get(req: HttpRequest) -> Result<HttpResponse> {
|
||||
// let cookies = cookies(req);
|
||||
|
||||
// let pref_nsfw: String = cookies.get("pref_nsfw").unwrap_or(&String::new()).to_owned();
|
||||
|
||||
// let s = SettingsTemplate { pref_nsfw }.render().unwrap();
|
||||
// Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
// }
|
||||
|
||||
// // Set cookies using response "Set-Cookie" header
|
||||
// pub async fn set(form: Form<Preferences>) -> HttpResponse {
|
||||
// let nsfw: Cookie = match &form.pref_nsfw {
|
||||
// Some(value) => Cookie::build("pref_nsfw", value).path("/").secure(true).http_only(true).finish(),
|
||||
// None => Cookie::build("pref_nsfw", "").finish(),
|
||||
// };
|
||||
|
||||
// let body = SettingsTemplate {
|
||||
// pref_nsfw: form.pref_nsfw.clone().unwrap_or_default(),
|
||||
// }
|
||||
// .render()
|
||||
// .unwrap();
|
||||
|
||||
// HttpResponse::Found()
|
||||
// .content_type("text/html")
|
||||
// .set_header("Set-Cookie", nsfw.to_string())
|
||||
// .set_header("Location", "/settings")
|
||||
// .body(body)
|
||||
// }
|
142
src/subreddit.rs
142
src/subreddit.rs
@ -1,8 +1,7 @@
|
||||
// CRATES
|
||||
use crate::utils::{fetch_posts, format_num, format_url, request, val, ErrorTemplate, Params, Post, Subreddit};
|
||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
||||
use crate::utils::{error, fetch_posts, format_num, format_url, param, request, rewrite_url, val, Post, Subreddit};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use std::convert::TryInto;
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -10,88 +9,97 @@ use std::convert::TryInto;
|
||||
struct SubredditTemplate {
|
||||
sub: Subreddit,
|
||||
posts: Vec<Post>,
|
||||
sort: String,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
#[allow(dead_code)]
|
||||
pub async fn page(web::Path(sub): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
||||
render(sub, params.sort.clone(), (params.before.clone(), params.after.clone())).await
|
||||
#[derive(Template)]
|
||||
#[template(path = "wiki.html", escape = "none")]
|
||||
struct WikiTemplate {
|
||||
sub: String,
|
||||
wiki: String,
|
||||
page: String,
|
||||
}
|
||||
|
||||
pub async fn render(sub_name: String, sort: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
|
||||
let sorting = sort.unwrap_or("hot".to_string());
|
||||
let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before
|
||||
// SERVICES
|
||||
pub async fn page(req: HttpRequest) -> HttpResponse {
|
||||
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
|
||||
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let url = match ends.0 {
|
||||
Some(val) => format!("https://www.reddit.com/r/{}/{}.json?before={}&count=25", sub_name, sorting, val),
|
||||
None => match ends.1 {
|
||||
Some(val) => format!("https://www.reddit.com/r/{}/{}.json?after={}&count=25", sub_name, sorting, val),
|
||||
None => format!("https://www.reddit.com/r/{}/{}.json", sub_name, sorting),
|
||||
},
|
||||
let sub_result = if !&sub.contains('+') && sub != "popular" {
|
||||
subreddit(&sub).await.unwrap_or_default()
|
||||
} else {
|
||||
Subreddit::default()
|
||||
};
|
||||
|
||||
let sub_result = subreddit(&sub_name).await;
|
||||
let items_result = fetch_posts(url, String::new()).await;
|
||||
|
||||
if sub_result.is_err() || items_result.is_err() {
|
||||
let s = ErrorTemplate {
|
||||
message: sub_result.err().unwrap().to_string(),
|
||||
match fetch_posts(&path, String::new()).await {
|
||||
Ok(items) => {
|
||||
let s = SubredditTemplate {
|
||||
sub: sub_result,
|
||||
posts: items.0,
|
||||
sort: (sort, param(&path, "t")),
|
||||
ends: (param(&path, "after"), items.1),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let mut sub = sub_result.unwrap();
|
||||
let items = items_result.unwrap();
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
}
|
||||
}
|
||||
|
||||
sub.icon = if sub.icon != "" {
|
||||
format!(r#"<img class="subreddit_icon" src="{}">"#, sub.icon)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
pub async fn wiki(req: HttpRequest) -> HttpResponse {
|
||||
let sub = req.match_info().get("sub").unwrap_or("reddit.com");
|
||||
let page = req.match_info().get("page").unwrap_or("index");
|
||||
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||
|
||||
let s = SubredditTemplate {
|
||||
sub: sub,
|
||||
posts: items.0,
|
||||
sort: sorting,
|
||||
ends: (before, items.1),
|
||||
match request(&path).await {
|
||||
Ok(res) => {
|
||||
let s = WikiTemplate {
|
||||
sub: sub.to_string(),
|
||||
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
|
||||
page: page.to_string(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
}
|
||||
}
|
||||
|
||||
// SUBREDDIT
|
||||
async fn subreddit(sub: &String) -> Result<Subreddit, &'static str> {
|
||||
async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
|
||||
// Build the Reddit JSON API url
|
||||
let url: String = format!("https://www.reddit.com/r/{}/about.json", sub);
|
||||
let path: String = format!("r/{}/about.json?raw_json=1", sub);
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
let req = request(url).await;
|
||||
// Send a request to the url
|
||||
match request(&path).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;
|
||||
|
||||
// If the Reddit API returns an error, exit this function
|
||||
if req.is_err() {
|
||||
return Err(req.err().unwrap());
|
||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
|
||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||
|
||||
let sub = Subreddit {
|
||||
name: val(&res, "display_name"),
|
||||
title: val(&res, "title"),
|
||||
description: val(&res, "public_description"),
|
||||
info: rewrite_url(&val(&res, "description_html").replace("\\", "")),
|
||||
icon: format_url(icon),
|
||||
members: format_num(members),
|
||||
active: format_num(active),
|
||||
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
||||
};
|
||||
|
||||
Ok(sub)
|
||||
}
|
||||
// If the Reddit API returns an error, exit this function
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
|
||||
// Otherwise, grab the JSON output from the request
|
||||
let res = req.unwrap();
|
||||
|
||||
let members = res["data"]["subscribers"].as_u64().unwrap_or(0);
|
||||
let active = res["data"]["accounts_active"].as_u64().unwrap_or(0);
|
||||
|
||||
let sub = Subreddit {
|
||||
name: val(&res, "display_name").await,
|
||||
title: val(&res, "title").await,
|
||||
description: val(&res, "public_description").await,
|
||||
icon: format_url(val(&res, "icon_img").await.as_str()).await,
|
||||
members: format_num(members.try_into().unwrap()),
|
||||
active: format_num(active.try_into().unwrap()),
|
||||
};
|
||||
|
||||
Ok(sub)
|
||||
}
|
||||
|
92
src/user.rs
92
src/user.rs
@ -1,7 +1,8 @@
|
||||
// CRATES
|
||||
use crate::utils::{fetch_posts, nested_val, request, ErrorTemplate, Params, Post, User};
|
||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
||||
use crate::utils::{error, fetch_posts, format_url, nested_val, param, request, Post, User};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -9,65 +10,68 @@ use askama::Template;
|
||||
struct UserTemplate {
|
||||
user: User,
|
||||
posts: Vec<Post>,
|
||||
sort: String,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
}
|
||||
|
||||
async fn render(username: String, sort: String) -> Result<HttpResponse> {
|
||||
// Build the Reddit JSON API url
|
||||
let url: String = format!("https://www.reddit.com/user/{}/.json?sort={}", username, sort);
|
||||
// FUNCTIONS
|
||||
pub async fn profile(req: HttpRequest) -> HttpResponse {
|
||||
// Build the Reddit JSON API path
|
||||
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||
|
||||
// Retrieve other variables from Libreddit request
|
||||
let sort = param(&path, "sort");
|
||||
let username = req.match_info().get("username").unwrap_or("").to_string();
|
||||
|
||||
// Request user profile data and user posts/comments from Reddit
|
||||
let user = user(&username).await;
|
||||
let posts = fetch_posts(url, "Comment".to_string()).await;
|
||||
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
||||
|
||||
if user.is_err() || posts.is_err() {
|
||||
let s = ErrorTemplate {
|
||||
message: user.err().unwrap().to_string(),
|
||||
match posts {
|
||||
Ok(items) => {
|
||||
let s = UserTemplate {
|
||||
user: user.unwrap(),
|
||||
posts: items.0,
|
||||
sort: (sort, param(&path, "t")),
|
||||
ends: (param(&path, "after"), items.1),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
||||
} else {
|
||||
let s = UserTemplate {
|
||||
user: user.unwrap(),
|
||||
posts: posts.unwrap().0,
|
||||
sort: sort,
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
}
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn page(web::Path(username): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
||||
match ¶ms.sort {
|
||||
Some(sort) => render(username, sort.to_string()).await,
|
||||
None => render(username, "hot".to_string()).await,
|
||||
// If there is an error show error page
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
}
|
||||
}
|
||||
|
||||
// USER
|
||||
async fn user(name: &String) -> Result<User, &'static str> {
|
||||
// Build the Reddit JSON API url
|
||||
let url: String = format!("https://www.reddit.com/user/{}/about.json", name);
|
||||
async fn user(name: &str) -> Result<User, &'static str> {
|
||||
// Build the Reddit JSON API path
|
||||
let path: String = format!("user/{}/about.json", name);
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
let req = request(url).await;
|
||||
let res;
|
||||
|
||||
// If the Reddit API returns an error, exit this function
|
||||
if req.is_err() {
|
||||
return Err(req.err().unwrap());
|
||||
// Send a request to the url
|
||||
match request(&path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => {
|
||||
res = response;
|
||||
}
|
||||
// If the Reddit API returns an error, exit this function
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
|
||||
// Otherwise, grab the JSON output from the request
|
||||
let res = req.unwrap();
|
||||
// Grab creation date as unix timestamp
|
||||
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
|
||||
// Parse the JSON output into a User struct
|
||||
Ok(User {
|
||||
name: name.to_string(),
|
||||
icon: nested_val(&res, "subreddit", "icon_img").await,
|
||||
karma: res["data"]["total_karma"].as_i64().unwrap(),
|
||||
banner: nested_val(&res, "subreddit", "banner_img").await,
|
||||
description: nested_val(&res, "subreddit", "public_description").await,
|
||||
title: nested_val(&res, "subreddit", "title"),
|
||||
icon: format_url(nested_val(&res, "subreddit", "icon_img")),
|
||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||
created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(),
|
||||
banner: nested_val(&res, "subreddit", "banner_img"),
|
||||
description: nested_val(&res, "subreddit", "public_description"),
|
||||
})
|
||||
}
|
||||
|
244
src/utils.rs
244
src/utils.rs
@ -1,39 +1,49 @@
|
||||
// use std::collections::HashMap;
|
||||
|
||||
//
|
||||
// CRATES
|
||||
//
|
||||
use chrono::{TimeZone, Utc};
|
||||
use serde_json::{from_str, Value};
|
||||
// use surf::{client, get, middleware::Redirect};
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use actix_web::{HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use base64::encode;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use regex::Regex;
|
||||
use serde_json::from_str;
|
||||
use url::Url;
|
||||
// use surf::{client, get, middleware::Redirect};
|
||||
|
||||
//
|
||||
// STRUCTS
|
||||
//
|
||||
#[allow(dead_code)]
|
||||
// Post flair with text, background color and foreground color
|
||||
pub struct Flair(pub String, pub String, pub String);
|
||||
// Post flags with nsfw and stickied
|
||||
pub struct Flags {
|
||||
pub nsfw: bool,
|
||||
pub stickied: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// Post containing content, metadata and media
|
||||
pub struct Post {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub community: String,
|
||||
pub body: String,
|
||||
pub author: String,
|
||||
pub author_flair: Flair,
|
||||
pub url: String,
|
||||
pub permalink: String,
|
||||
pub score: String,
|
||||
pub upvote_ratio: i64,
|
||||
pub post_type: String,
|
||||
pub flair: Flair,
|
||||
pub flags: Flags,
|
||||
pub media: String,
|
||||
pub time: String,
|
||||
pub flair: Flair,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// Comment with content, post, score and data/time that it was posted
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
pub body: String,
|
||||
pub author: String,
|
||||
pub flair: Flair,
|
||||
@ -42,37 +52,42 @@ pub struct Comment {
|
||||
pub replies: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// User struct containing metadata about user
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub icon: String,
|
||||
pub karma: i64,
|
||||
pub created: String,
|
||||
pub banner: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Default)]
|
||||
// Subreddit struct containing metadata about community
|
||||
pub struct Subreddit {
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub info: String,
|
||||
pub icon: String,
|
||||
pub members: String,
|
||||
pub active: String,
|
||||
pub wiki: bool,
|
||||
}
|
||||
|
||||
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct Params {
|
||||
pub t: Option<String>,
|
||||
pub q: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub after: Option<String>,
|
||||
pub before: Option<String>,
|
||||
}
|
||||
|
||||
// Error template
|
||||
#[derive(askama::Template)]
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html", escape = "none")]
|
||||
pub struct ErrorTemplate {
|
||||
pub message: String,
|
||||
@ -82,14 +97,47 @@ pub struct ErrorTemplate {
|
||||
// FORMATTING
|
||||
//
|
||||
|
||||
pub async fn format_url(url: &str) -> String {
|
||||
#[cfg(feature = "proxy")]
|
||||
return "/proxy/".to_string() + encode(url).as_str();
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
return url.to_string();
|
||||
// Grab a query param from a url
|
||||
pub fn param(path: &str, value: &str) -> String {
|
||||
let url = Url::parse(format!("https://libredd.it/{}", path).as_str()).unwrap();
|
||||
let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
|
||||
pairs.get(value).unwrap_or(&String::new()).to_owned()
|
||||
}
|
||||
|
||||
// Cookies from request
|
||||
// pub fn cookies(req: HttpRequest) -> HashMap<String, String> {
|
||||
// let mut result: HashMap<String, String> = HashMap::new();
|
||||
|
||||
// let cookies: Vec<Cookie> = req
|
||||
// .headers()
|
||||
// .get_all("Cookie")
|
||||
// .map(|value| value.to_str().unwrap())
|
||||
// .map(|unparsed| Cookie::parse(unparsed).unwrap())
|
||||
// .collect();
|
||||
|
||||
// for cookie in cookies {
|
||||
// result.insert(cookie.name().to_string(), cookie.value().to_string());
|
||||
// }
|
||||
|
||||
// result
|
||||
// }
|
||||
|
||||
// Direct urls to proxy if proxy is enabled
|
||||
pub fn format_url(url: String) -> String {
|
||||
if url.is_empty() || url == "self" || url == "default" {
|
||||
String::new()
|
||||
} else {
|
||||
format!("/proxy/{}", encode(url).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite Reddit links to Libreddit in body of text
|
||||
pub fn rewrite_url(text: &str) -> String {
|
||||
let re = Regex::new(r#"href="(https://|http://|)(www.|)(reddit).(com)/"#).unwrap();
|
||||
re.replace_all(text, r#"href="/"#).to_string()
|
||||
}
|
||||
|
||||
// Append `m` and `k` for millions and thousands respectively
|
||||
pub fn format_num(num: i64) -> String {
|
||||
if num > 1000000 {
|
||||
format!("{}m", num / 1000000)
|
||||
@ -104,119 +152,125 @@ pub fn format_num(num: i64) -> String {
|
||||
// JSON PARSING
|
||||
//
|
||||
|
||||
#[allow(dead_code)]
|
||||
// val() function used to parse JSON from Reddit APIs
|
||||
pub async fn val(j: &serde_json::Value, k: &str) -> String {
|
||||
String::from(j["data"][k].as_str().unwrap_or(""))
|
||||
pub fn val(j: &serde_json::Value, k: &str) -> String {
|
||||
String::from(j["data"][k].as_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// nested_val() function used to parse JSON from Reddit APIs
|
||||
pub async fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
|
||||
String::from(j["data"][n][k].as_str().unwrap())
|
||||
pub fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
|
||||
String::from(j["data"][n][k].as_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
|
||||
// Send a request to the url, receive JSON in response
|
||||
let req = request(url).await;
|
||||
// Fetch posts of a user or subreddit
|
||||
pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
|
||||
let res;
|
||||
let post_list;
|
||||
|
||||
// If the Reddit API returns an error, exit this function
|
||||
if req.is_err() {
|
||||
return Err(req.err().unwrap());
|
||||
// Send a request to the url
|
||||
match request(&path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => {
|
||||
res = response;
|
||||
}
|
||||
// If the Reddit API returns an error, exit this function
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
|
||||
// Otherwise, grab the JSON output from the request
|
||||
let res = req.unwrap();
|
||||
|
||||
// Fetch the list of posts from the JSON response
|
||||
let post_list = res["data"]["children"].as_array().unwrap();
|
||||
match res["data"]["children"].as_array() {
|
||||
Some(list) => post_list = list,
|
||||
None => return Err("No posts found"),
|
||||
}
|
||||
|
||||
let mut posts: Vec<Post> = Vec::new();
|
||||
|
||||
for post in post_list.iter() {
|
||||
let img = if val(post, "thumbnail").await.starts_with("https:/") {
|
||||
format_url(val(post, "thumbnail").await.as_str()).await
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||
let score = post["data"]["score"].as_i64().unwrap();
|
||||
let title = val(post, "title").await;
|
||||
// For each post from posts list
|
||||
for post in post_list {
|
||||
let img = format_url(val(post, "thumbnail"));
|
||||
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
|
||||
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
let title = val(post, "title");
|
||||
|
||||
posts.push(Post {
|
||||
id: val(post, "id"),
|
||||
title: if title.is_empty() { fallback_title.to_owned() } else { title },
|
||||
community: val(post, "subreddit").await,
|
||||
body: val(post, "body").await,
|
||||
author: val(post, "author").await,
|
||||
community: val(post, "subreddit"),
|
||||
body: rewrite_url(&val(post, "body_html")),
|
||||
author: val(post, "author"),
|
||||
author_flair: Flair(
|
||||
val(post, "author_flair_text").await,
|
||||
val(post, "author_flair_background_color").await,
|
||||
val(post, "author_flair_text_color").await,
|
||||
val(post, "author_flair_text"),
|
||||
val(post, "author_flair_background_color"),
|
||||
val(post, "author_flair_text_color"),
|
||||
),
|
||||
score: format_num(score),
|
||||
upvote_ratio: ratio as i64,
|
||||
post_type: "link".to_string(),
|
||||
media: img,
|
||||
url: val(post, "permalink").await,
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
|
||||
flair: Flair(
|
||||
val(post, "link_flair_text").await,
|
||||
val(post, "link_flair_background_color").await,
|
||||
if val(post, "link_flair_text_color").await == "dark" {
|
||||
val(post, "link_flair_text"),
|
||||
val(post, "link_flair_background_color"),
|
||||
if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
} else {
|
||||
"white".to_string()
|
||||
},
|
||||
),
|
||||
flags: Flags {
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or_default(),
|
||||
},
|
||||
permalink: val(post, "permalink"),
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or("").to_string()))
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||
}
|
||||
|
||||
//
|
||||
// NETWORKING
|
||||
//
|
||||
|
||||
pub async fn error(msg: String) -> HttpResponse {
|
||||
let body = ErrorTemplate { message: msg }.render().unwrap_or_default();
|
||||
HttpResponse::NotFound().content_type("text/html").body(body)
|
||||
}
|
||||
|
||||
// Make a request to a Reddit API and parse the JSON response
|
||||
#[allow(dead_code)]
|
||||
pub async fn request(url: String) -> Result<serde_json::Value, &'static str> {
|
||||
// --- actix-web::client ---
|
||||
// let client = actix_web::client::Client::default();
|
||||
// let res = client
|
||||
// .get(url)
|
||||
// .send()
|
||||
// .await?
|
||||
// .body()
|
||||
// .limit(1000000)
|
||||
// .await?;
|
||||
pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
|
||||
let url = format!("https://www.reddit.com/{}", path);
|
||||
|
||||
// let body = std::str::from_utf8(res.as_ref())?; // .as_ref converts Bytes to [u8]
|
||||
|
||||
// --- surf ---
|
||||
// let req = get(&url).header("User-Agent", "libreddit");
|
||||
// let client = client().with(Redirect::new(5));
|
||||
// let mut res = client.send(req).await.unwrap();
|
||||
// let success = res.status().is_success();
|
||||
// let body = res.body_string().await.unwrap();
|
||||
|
||||
// --- reqwest ---
|
||||
let res = reqwest::get(&url).await.unwrap();
|
||||
// Read the status from the response
|
||||
let success = res.status().is_success();
|
||||
// Read the body of the response
|
||||
let body = res.text().await.unwrap();
|
||||
|
||||
// Parse the response from Reddit as JSON
|
||||
let json: Value = from_str(body.as_str()).unwrap_or(Value::Null);
|
||||
|
||||
if !success {
|
||||
println!("! {} - {}", url, "Page not found");
|
||||
Err("Page not found")
|
||||
} else if json == Value::Null {
|
||||
println!("! {} - {}", url, "Failed to parse page JSON data");
|
||||
Err("Failed to parse page JSON data")
|
||||
} else {
|
||||
Ok(json)
|
||||
// Send request using reqwest
|
||||
match reqwest::get(&url).await {
|
||||
Ok(res) => {
|
||||
// Read the status from the response
|
||||
match res.status().is_success() {
|
||||
true => {
|
||||
// Parse the response from Reddit as JSON
|
||||
match from_str(res.text().await.unwrap_or_default().as_str()) {
|
||||
Ok(json) => Ok(json),
|
||||
Err(_) => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - Failed to parse page JSON data", url));
|
||||
Err("Failed to parse page JSON data")
|
||||
}
|
||||
}
|
||||
}
|
||||
// If Reddit returns error, tell user Page Not Found
|
||||
false => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - Page not found", url));
|
||||
Err("Page not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
// If can't send request to Reddit, return this to user
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - {}", url, e));
|
||||
Err("Couldn't send request to Reddit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
User-agent: *
|
||||
Allow: /
|
508
static/style.css
508
static/style.css
@ -1,6 +1,7 @@
|
||||
/* General */
|
||||
|
||||
:root {
|
||||
--accent: aqua;
|
||||
--background: #0F0F0F;
|
||||
--foreground: #222;
|
||||
--outside: #1F1F1F;
|
||||
@ -9,37 +10,48 @@
|
||||
--black-contrast: 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--background);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
* {
|
||||
transition: 0.2s all;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
header {
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: aqua;
|
||||
align-items: center;
|
||||
color: var(--accent);
|
||||
background: var(--outside);
|
||||
padding: 15px;
|
||||
font-weight: bold;
|
||||
padding: 5px 15px;
|
||||
font-size: 20px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
#lib, #github {
|
||||
color: white;
|
||||
}
|
||||
nav #lib, nav #github, nav #version { color: white; }
|
||||
nav #version { opacity: 25%; }
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 1000px;
|
||||
padding: 10px 20px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
#column_one {
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
margin-top: 25px;
|
||||
padding: 0px 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -47,109 +59,241 @@ footer {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
footer > a {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
a:not(.post_right):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#about {
|
||||
padding-top: 20px;
|
||||
background: #151515;
|
||||
img[src=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#version {
|
||||
color: white;
|
||||
opacity: 25%;
|
||||
aside {
|
||||
flex-grow: 1;
|
||||
margin: 20px 20px 0 10px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
/* Subreddit */
|
||||
.panel {
|
||||
border: 1px solid var(--highlighted);
|
||||
}
|
||||
|
||||
.subreddit {
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
.dot {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* User & Subreddit */
|
||||
|
||||
#user, #subreddit, #sidebar {
|
||||
margin: 40px auto 0 auto;
|
||||
display: flex;
|
||||
padding-bottom: 25px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: max-content;
|
||||
background: var(--outside);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.subreddit_name {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#user *, #subreddit * { text-align: center; }
|
||||
|
||||
.subreddit_right {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
}
|
||||
#user, #sub_meta, #sidebar_contents { padding: 20px; }
|
||||
|
||||
.subreddit_icon {
|
||||
#sidebar, #sidebar_contents { margin-top: 10px; }
|
||||
#sidebar_label { padding: 10px; }
|
||||
|
||||
#user_icon, #sub_icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: 100%;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#stats {
|
||||
margin-top: 10px;
|
||||
#user_title, #sub_title {
|
||||
margin: 0 20px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* User */
|
||||
#user_description, #sub_description {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.user {
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
#user_name, #user_description:not(:empty), #user_icon
|
||||
#sub_name, #sub_icon, #sub_description:not(:empty) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#user_details, #sub_details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
|
||||
#user_details > label, #sub_details > label {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Wiki Pages */
|
||||
|
||||
#wiki {
|
||||
background: var(--foreground);
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
#top {
|
||||
background: var(--highlighted);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.user_right {
|
||||
#top > * {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
#top > div {
|
||||
border-bottom: 2px solid white;
|
||||
}
|
||||
|
||||
/* Sorting and Search */
|
||||
|
||||
select {
|
||||
background: var(--outside);
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
select, #search {
|
||||
border: none;
|
||||
padding: 0 15px;
|
||||
height: 40px;
|
||||
appearance: none;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#searchbox {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
box-shadow: var(--black-contrast);
|
||||
}
|
||||
|
||||
.user_icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 100%;
|
||||
padding: 20px;
|
||||
#searchbox > *, #sort_submit {
|
||||
background: var(--highlighted);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Sorting */
|
||||
#search {
|
||||
border-right: 2px var(--outside) solid;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#sort {
|
||||
max-width: 750px;
|
||||
margin: 20px -10px;
|
||||
#inside {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
padding: 0px 10px;
|
||||
align-items: center;
|
||||
border-right: 2px var(--outside) solid;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#sort > div, footer > a {
|
||||
#restrict_sr { margin-right: 5px; }
|
||||
|
||||
input[type="submit"] {
|
||||
border: 0;
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
select:hover { background: var(--foreground); }
|
||||
input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
#timeframe {
|
||||
margin: 0 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#sort_options + #timeframe:not(#search_sort > #timeframe) {
|
||||
margin-left: 10px;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#search_sort {
|
||||
background: var(--highlighted);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#search_sort > #search {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#search_sort > *, #searchbox > * { font-size: 15px; }
|
||||
|
||||
#search_sort > :not(:first-child), #search_sort > #sort_options {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-right: 0;
|
||||
border-left: 2px solid var(--background);
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#sort_options {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#sort, #search_sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#sort_options, footer > a {
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--black-contrast);
|
||||
background: var(--outside);
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#sort_options > a, footer > a {
|
||||
color: lightgrey;
|
||||
border-radius: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
#sort > div.selected {
|
||||
background: aqua;
|
||||
#sort_options > a.selected {
|
||||
background: var(--accent);
|
||||
color: black;
|
||||
}
|
||||
|
||||
#sort > div:hover {
|
||||
#sort_options > a:not(.selected):hover {
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
@ -160,6 +304,17 @@ a:not(.post_right):hover {
|
||||
background: var(--post);
|
||||
box-shadow: var(--black-contrast);
|
||||
display: flex;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
.post:not(:last-child) { margin-bottom: 10px; }
|
||||
|
||||
.post.highlighted {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.post.highlighted > .post_right {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post:hover {
|
||||
@ -172,16 +327,39 @@ a:not(.post_right):hover {
|
||||
|
||||
.post_left, .post_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-wrap: anywhere;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.post_left {
|
||||
text-align: center;
|
||||
background: var(--foreground);
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
border-radius: 5px 0 0 5px;
|
||||
flex-direction: column;
|
||||
min-width: 50px;
|
||||
padding: 5px;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 20px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#post_footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
opacity: 0.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#post_links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#post_links > li {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.post_subreddit {
|
||||
@ -189,36 +367,31 @@ a:not(.post_right):hover {
|
||||
}
|
||||
|
||||
.post_title {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 1em;
|
||||
color: aqua;
|
||||
.post_text {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post_right {
|
||||
padding: 20px 25px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.post_right > * {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.post_right > p {
|
||||
opacity: 0.75;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post_media {
|
||||
max-width: 90%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.post_media[src=""] {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.post_body {
|
||||
@ -227,49 +400,50 @@ a:not(.post_right):hover {
|
||||
margin: 10px 5px;
|
||||
}
|
||||
|
||||
.post_body > p:not(:first-child) {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.post_body a {
|
||||
text-decoration: underline;
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
#post_url {
|
||||
color: aqua;
|
||||
color: var(--accent);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.post_thumbnail {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--foreground);
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.post_thumbnail[src=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.post_flair {
|
||||
background: aqua;
|
||||
background: var(--accent);
|
||||
color: black;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
color: #FF5C5D;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #FF5C5D;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stickied {
|
||||
--accent: #5cff85;
|
||||
border: 1px solid #5cff85;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
|
||||
.comment {
|
||||
margin-top: 1em;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.comment_left, .comment_right {
|
||||
@ -280,16 +454,17 @@ a:not(.post_right):hover {
|
||||
.comment_left {
|
||||
text-align: center;
|
||||
min-width: 50px;
|
||||
padding: 5px 0px;
|
||||
padding: 5px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment_title {
|
||||
font-size: 20px;
|
||||
}
|
||||
.comment_title { font-size: 20px; }
|
||||
.comment_link { text-decoration: underline; }
|
||||
.comment_author { opacity: 0.9; }
|
||||
|
||||
.comment_author {
|
||||
opacity: 0.9;
|
||||
.comment_author.op {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.author_flair {
|
||||
@ -302,24 +477,17 @@ a:not(.post_right):hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment_upvote {
|
||||
margin-top: 0.5em;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
background: var(--foreground);
|
||||
width: 40px;
|
||||
padding: 10px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.comment_subreddit {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment_score {
|
||||
color: aqua;
|
||||
color: var(--accent);
|
||||
background: var(--foreground);
|
||||
min-width: 40px;
|
||||
border-radius: 5px;
|
||||
padding: 10px 0px;
|
||||
padding: 10px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.comment_right {
|
||||
@ -330,7 +498,7 @@ a:not(.post_right):hover {
|
||||
}
|
||||
|
||||
.comment_data > * {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.comment_image {
|
||||
@ -338,10 +506,6 @@ a:not(.post_right):hover {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.comment_image[src=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment_body {
|
||||
opacity: 0.9;
|
||||
font-weight: normal;
|
||||
@ -349,25 +513,30 @@ a:not(.post_right):hover {
|
||||
}
|
||||
|
||||
.comment_body > p:not(:first-child) {
|
||||
margin-top: 1.5em;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment_body a {
|
||||
text-decoration: underline;
|
||||
color: aqua;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.deeper_replies {
|
||||
color: var(--accent);
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
::marker {
|
||||
color: aqua;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.reply {
|
||||
margin-top: 0;
|
||||
margin-left: 2em;
|
||||
.replies > .comment {
|
||||
margin-left: -20px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
opacity: 0.75;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.line {
|
||||
@ -385,9 +554,33 @@ a:not(.post_right):hover {
|
||||
background: black;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
/* Markdown */
|
||||
|
||||
pre {
|
||||
.md > *:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.md h1 { font-size: 22px; }
|
||||
.md h2 { font-size: 20px; }
|
||||
.md h3 { font-size: 18px; }
|
||||
.md h4 { font-size: 16px; }
|
||||
.md h5 { font-size: 14px; }
|
||||
.md h6 { font-size: 12px; }
|
||||
|
||||
.md blockquote {
|
||||
padding-left: 6px;
|
||||
margin: 4px 0 4px 5px;
|
||||
border-left: 4px solid var(--highlighted);
|
||||
}
|
||||
|
||||
.md a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.md li { margin: 10px 0; }
|
||||
.toc_child { list-style: none; }
|
||||
|
||||
.md pre {
|
||||
background: var(--outside);
|
||||
padding: 20px;
|
||||
margin-top: 10px;
|
||||
@ -395,21 +588,27 @@ pre {
|
||||
box-shadow: var(--black-contrast);
|
||||
}
|
||||
|
||||
code {
|
||||
.md table {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.md code {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.md code:not(.md pre > code) { background: var(--highlighted); }
|
||||
|
||||
/* Tables */
|
||||
|
||||
table {
|
||||
border: 3px var(--highlighted) solid;
|
||||
border-spacing: 0rem;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px var(--highlighted) solid;
|
||||
padding: 0.5em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@ -419,23 +618,54 @@ td, th {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.post_left {
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
.post_header {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.post_right {
|
||||
padding: 20px;
|
||||
.post_left {
|
||||
border-radius: 0 0 5px 5px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 0;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.post_thumbnail {
|
||||
max-width: initial;
|
||||
.replies > .comment {
|
||||
margin-left: -25px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
main {
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
aside, #subreddit, #user {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#user, #sidebar { margin: 20px 0; }
|
||||
#logo { margin: 5px auto; }
|
||||
#searchbox { width: 100%; }
|
||||
#github { display: none; }
|
||||
}
|
||||
|
@ -2,30 +2,24 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';">
|
||||
<title>{% block title %}Libreddit{% endblock %}</title>
|
||||
<meta http-equiv="Referrer-Policy" content="no-referrer">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
{% block sortstyle %}
|
||||
<style>
|
||||
#sort > #sort_{{ sort }} {
|
||||
background: aqua;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
{% block header %}
|
||||
<header>
|
||||
<a href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
||||
<!-- NAVIGATION BAR -->
|
||||
<nav>
|
||||
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
||||
{% block search %}{% endblock %}
|
||||
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||
</header>
|
||||
{% endblock %}
|
||||
</nav>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
{% block body %}
|
||||
<main>
|
||||
{% block content %}
|
||||
|
@ -1,44 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Libreddit{% endblock %}
|
||||
{% block content %}
|
||||
<div id="sort">
|
||||
<div id="sort_hot"><a href="?sort=hot">Hot</a></div>
|
||||
<div id="sort_top"><a href="?sort=top">Top</a></div>
|
||||
<div id="sort_new"><a href="?sort=new">New</a></div>
|
||||
<div id="sort_rising"><a href="?sort=rising">Rising</a></div>
|
||||
</div>
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<div class="post_left">
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
{% endfor %}
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endblock %}
|
@ -1,82 +1,121 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}{{ post.title }} - r/{{ post.community }}{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search(["/r/", post.community.as_str()].concat(), "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block root %}/r/{{ post.community }}{% endblock %}{% block location %}r/{{ post.community }}{% endblock %}
|
||||
{% block head %}
|
||||
{% call super() %}
|
||||
<meta name="author" content="u/{{ post.author }}">
|
||||
{% endblock %}
|
||||
|
||||
<!-- OPEN COMMENT MACRO -->
|
||||
{% macro comment(item) -%}
|
||||
|
||||
<div class="comment">
|
||||
<div id="{{ item.id }}" class="comment">
|
||||
<div class="comment_left">
|
||||
<h3 class="comment_score">{{ item.score }}</h3>
|
||||
<p class="comment_score">{{ item.score }}</p>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_author" href="/u/{{ item.author }}">u/{{ item.author }}</a>
|
||||
<summary class="comment_data"><a class="comment_author {% if item.author == post.author %}op{% endif %}" href="/u/{{ item.author }}">u/{{ item.author }}</a>
|
||||
{% if item.flair.0 != "" %}
|
||||
<small class="author_flair">{{ item.flair.0 }}</small>
|
||||
{% endif %}
|
||||
• <span class="datetime">{{ item.time }}</span>
|
||||
<span class="datetime">{{ item.time }}</span>
|
||||
</summary>
|
||||
<h4 class="comment_body">{{ item.body }}</h4>
|
||||
|
||||
<p class="comment_body">{{ item.body }}</p>
|
||||
{%- endmacro %}
|
||||
|
||||
<!-- CLOSE COMMENT MACRO -->
|
||||
{% macro close() %}
|
||||
</details></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="post highlighted">
|
||||
<div class="post_left">
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
•
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<a href="{{ post.url }}" class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type == "video" %}
|
||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
|
||||
{% else if post.post_type == "link" %}
|
||||
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
||||
{% endif %}
|
||||
<h4 class="post_body">{{ post.body }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sort">
|
||||
<div id="sort_confidence"><a href="?sort=confidence">Best</a></div>
|
||||
<div id="sort_top"><a href="?sort=top">Top</a></div>
|
||||
<div id="sort_new"><a href="?sort=new">New</a></div>
|
||||
<div id="sort_controversial"><a href="?sort=controversial">Controversial</a></div>
|
||||
<div id="sort_old"><a href="?sort=old">Old</a></div>
|
||||
</div>
|
||||
<div id="column_one">
|
||||
|
||||
{% for c in comments -%}
|
||||
<!-- POST CONTENT -->
|
||||
<div class="post highlighted panel">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</p>
|
||||
<a href="{{ post.permalink }}" class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<!-- POST MEDIA -->
|
||||
{% if post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type == "video" %}
|
||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
|
||||
{% else if post.post_type == "link" %}
|
||||
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- POST BODY -->
|
||||
<div class="post_body">{{ post.body }}</div>
|
||||
<div id="post_footer">
|
||||
<ul id="post_links">
|
||||
<li><a href="/{{ post.id }}">permalink</a></li>
|
||||
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li>
|
||||
</ul>
|
||||
<p>{{ post.upvote_ratio }}% Upvoted</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SORT FORM -->
|
||||
<form id="sort">
|
||||
<select name="sort">
|
||||
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
|
||||
</select><input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
|
||||
<!-- COMMENTS -->
|
||||
{% for c in comments -%}
|
||||
<div class="thread">
|
||||
<!-- EACH COMMENT -->
|
||||
{% call comment(c) %}
|
||||
<div class="replies">
|
||||
{% for reply in c.replies %}
|
||||
{% call comment(reply) %}
|
||||
<div class="replies">
|
||||
{% for response in reply.replies %}
|
||||
{% call comment(response) %}</details></div>
|
||||
{% endfor %}
|
||||
</div></details></div>
|
||||
<div class="replies">{% for reply1 in c.replies %}{% call comment(reply1) %}
|
||||
<!-- FIRST-LEVEL REPLIES -->
|
||||
<div class="replies">{% for reply2 in reply1.replies %}{% call comment(reply2) %}
|
||||
<!-- SECOND-LEVEL REPLIES -->
|
||||
<div class="replies">{% for reply3 in reply2.replies %}{% call comment(reply3) %}
|
||||
<!-- THIRD-LEVEL REPLIES -->
|
||||
{% if reply3.replies.len() > 0 %}
|
||||
<!-- LINK TO CONTINUE REPLIES -->
|
||||
<a class="deeper_replies" href="{{ post.permalink }}{{ reply3.id }}">→ More replies</a>
|
||||
{% endif %}
|
||||
{% call close() %}
|
||||
{% endfor %}
|
||||
</div>{% call close() %}
|
||||
{% endfor %}
|
||||
</div></details></div>
|
||||
</div>{% call close() %}
|
||||
{% endfor %}
|
||||
</div>{% call close() %}
|
||||
</div>
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
82
templates/search.html
Normal file
82
templates/search.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}Libreddit: search results - {{ params.q }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="column_one">
|
||||
<form id="search_sort">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q }}">
|
||||
{% if sub != "" %}
|
||||
<div id="inside">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||
<label for="restrict_sr">in r/{{ sub }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<select id="sort_options" name="sort">
|
||||
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||
</select>{% if params.sort != "new" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
{% for post in posts %}
|
||||
{% if post.title != "Comment" %}
|
||||
<div class="post panel">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score">{{ post.score }}</p>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body }}</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<footer>
|
||||
{% if params.before != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&before={{ params.before }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if params.after != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&after={{ params.after }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% endblock %}
|
18
templates/settings.html
Normal file
18
templates/settings.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}Libreddit Settings{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search("".to_owned(), "", "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
<form action="/settings/save" method="POST">
|
||||
<label for="pref_nsfw">NSFW</label>
|
||||
<input type="checkbox" name="pref_nsfw" id="pref_nsfw" {% if pref_nsfw == "on" %}checked{% endif %}>
|
||||
<input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
</main>
|
||||
{% endblock %}
|
@ -1,57 +1,96 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}r/{{ sub.name }}: {{ sub.description }}{% endblock %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}
|
||||
{% if sub.title != "" %}{{ sub.title }}
|
||||
{% else if sub.name != "" %}{{ sub.name }}
|
||||
{% else %}Libreddit{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search(["/r/", sub.name.as_str()].concat(), "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="about">
|
||||
<div class="subreddit">
|
||||
<div class="subreddit_left">
|
||||
{{ sub.icon }}
|
||||
</div>
|
||||
<div class="subreddit_right">
|
||||
<h2 class="subreddit_name">r/{{ sub.name }}</h2>
|
||||
<p class="subreddit_description">{{ sub.description }}</p>
|
||||
<div id="stats">👤 {{ sub.members }} 🟢 {{ sub.active }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div id="sort">
|
||||
<div id="sort_hot"><a href="?sort=hot">Hot</a></div>
|
||||
<div id="sort_top"><a href="?sort=top">Top</a></div>
|
||||
<div id="sort_new"><a href="?sort=new">New</a></div>
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<div id="sort_options">
|
||||
{% if sub.name.is_empty() %}
|
||||
{% call utils::sort("", ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||
{% else %}
|
||||
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
||||
<input id="sort_submit" type="submit" value="→">
|
||||
</select>{% endif %}
|
||||
</form>
|
||||
{% for post in posts %}
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %} panel">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
<span class="dot">•</span>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<div class="post_left">
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
{% if sub.name != "" %}
|
||||
<aside>
|
||||
<div class="panel" id="subreddit">
|
||||
{% if sub.wiki %}
|
||||
<div id="top">
|
||||
<div>Posts</div>
|
||||
<a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="sub_meta">
|
||||
<img id="sub_icon" src="{{ sub.icon }}">
|
||||
<p id="sub_title">{{ sub.title }}</p>
|
||||
<p id="sub_name">r/{{ sub.name }}</p>
|
||||
<p id="sub_description">{{ sub.description }}</p>
|
||||
<div id="sub_details">
|
||||
<label>Members</label>
|
||||
<label>Active</label>
|
||||
<div>{{ sub.members }}</div>
|
||||
<div>{{ sub.active }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ sub.name }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
{% endfor %}
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
<details class="panel" id="sidebar">
|
||||
<summary id="sidebar_label">Sidebar</summary>
|
||||
<div id="sidebar_contents">{{ sub.info }}</div>
|
||||
</details>
|
||||
</aside>
|
||||
{% endif %}
|
||||
</main>
|
||||
{% endblock %}
|
@ -1,63 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Libreddit: u/{{ user.name }}{% endblock %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search("".to_owned(), "", "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Libreddit{% endblock %}
|
||||
{% block body %}
|
||||
<div id="about">
|
||||
<div class="user">
|
||||
<div class="user_left">
|
||||
<img class="user_icon" src="{{ user.icon }}">
|
||||
<main style="max-width: 1000px;">
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<select name="sort">
|
||||
{% call utils::options(sort.0, ["hot", "new", "top"], "") %}
|
||||
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
{% for post in posts %}
|
||||
{% if post.title != "Comment" %}
|
||||
<div class="post panel">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 == "Comment" %}
|
||||
{% else if post.flair.0 == "" %}
|
||||
{% else %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="user_right">
|
||||
<h2 class="user_name">u/{{ user.name }}</h2>
|
||||
<p class="user_description"><span>Karma:</span> {{ user.karma }} | <span>Description:</span> "{{ user.description }}"</p>
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score">{{ post.score }}</p>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body }}</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div id="sort">
|
||||
<div id="sort_hot"><a href="?sort=hot">Hot</a></div>
|
||||
<div id="sort_top"><a href="?sort=top">Top</a></div>
|
||||
<div id="sort_new"><a href="?sort=new">New</a></div>
|
||||
</div>
|
||||
{% for post in posts %}
|
||||
{% if post.title != "Comment" %}
|
||||
<div class='post'>
|
||||
<div class="post_left">
|
||||
<h3 class="post_score">{{ post.score }}</h3>
|
||||
<aside>
|
||||
<div class="panel" id="user">
|
||||
<img id="user_icon" src="{{ user.icon }}">
|
||||
<p id="user_title">{{ user.title }}</p>
|
||||
<p id="user_name">u/{{ user.name }}</p>
|
||||
<div id="user_description">{{ user.description }}</div>
|
||||
<div id="user_details">
|
||||
<label>Karma</label>
|
||||
<label>Created</label>
|
||||
<div>{{ user.karma }}</div>
|
||||
<div>{{ user.created }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<h4>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h3 class="post_title">
|
||||
{% if post.flair.0 == "Comment" %}
|
||||
{% else if post.flair.0 == "" %}
|
||||
{% else %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<h3 class="comment_score">{{ post.score }}</h3>
|
||||
</div>
|
||||
<div class="comment_right">
|
||||
<h4>
|
||||
COMMENT
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</h4>
|
||||
<h4 class="comment_body">{{ post.body }}</h4>
|
||||
</div>
|
||||
</div><br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</aside>
|
||||
</main>
|
||||
{% endblock %}
|
28
templates/utils.html
Normal file
28
templates/utils.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% macro options(current, values, default) -%}
|
||||
{% for value in values %}
|
||||
<option value="{{ value }}" {% if current == value || (current == "" && value == default) %}selected{% endif %}>
|
||||
{{ format!("{}{}", value.get(0..1).unwrap().to_uppercase(), value.get(1..).unwrap()) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sort(root, methods, selected) -%}
|
||||
{% for method in methods %}
|
||||
<a {% if method == selected %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
|
||||
{{ format!("{}{}", method.get(0..1).unwrap().to_uppercase(), method.get(1..).unwrap()) }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro search(root, search) -%}
|
||||
<form action="{% if root != "/r/" && !root.is_empty() %}{{ root }}{% endif %}/search/" id="searchbox">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
|
||||
{% if root != "/r/" && !root.is_empty() %}
|
||||
<div id="inside">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr">
|
||||
<label for="restrict_sr">in {{ root }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="submit" value="→">
|
||||
</form>
|
||||
{%- endmacro %}
|
25
templates/wiki.html
Normal file
25
templates/wiki.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}
|
||||
{% if sub != "" %}{{ page }} - {{ sub }}
|
||||
{% else %}Libreddit{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search(["/r/", sub.as_str()].concat(), "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
<div class="panel" id="column_one">
|
||||
<div id="top">
|
||||
<a href="/r/{{ sub }}">Posts</a>
|
||||
<div>Wiki</div>
|
||||
</div>
|
||||
<div id="wiki">
|
||||
{{ wiki }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user