diff --git a/Cargo.lock b/Cargo.lock index be43310..3e257f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,15 +346,6 @@ dependencies = [ "toml", ] -[[package]] -name = "async-mutex" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" -dependencies = [ - "event-listener", -] - [[package]] name = "async-recursion" version = "0.3.1" @@ -489,9 +480,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" +checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" [[package]] name = "byteorder" @@ -520,40 +511,6 @@ dependencies = [ "bytes 1.0.1", ] -[[package]] -name = "cached" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2afe73808fbaac302e39c9754bfc3c4b4d0f99c9c240b9f4e4efc841ad1b74" -dependencies = [ - "async-mutex", - "async-trait", - "cached_proc_macro", - "cached_proc_macro_types", - "futures", - "hashbrown", - "once_cell", -] - -[[package]] -name = "cached_proc_macro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf857ae42d910aede5c5186e62684b0d7a597ce2fe3bd14448ab8f7ef439848c" -dependencies = [ - "async-mutex", - "cached_proc_macro_types", - "darling", - "quote", - "syn", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" - [[package]] name = "cc" version = "1.0.66" @@ -574,9 +531,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chunked_transfer" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7477065d45a8fe57167bf3cf8bcd3729b54cfcb81cca49bda2d038ea89ae82ca" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "const_fn" @@ -616,41 +573,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "0.99.11" @@ -704,17 +626,11 @@ dependencies = [ "syn", ] -[[package]] -name = "event-listener" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" - [[package]] name = "flate2" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" dependencies = [ "cfg-if 1.0.0", "crc32fast", @@ -969,12 +885,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.2.0" @@ -1034,9 +944,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" dependencies = [ "wasm-bindgen", ] @@ -1078,19 +988,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.82" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff" [[package]] name = "libreddit" -version = "0.2.8" +version = "0.2.9" dependencies = [ "actix-web", "askama", "async-recursion", "base64 0.13.0", - "cached", "futures", "regex", "serde", @@ -1117,11 +1026,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] @@ -1222,9 +1131,9 @@ dependencies = [ [[package]] name = "nom" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1" dependencies = [ "bitvec", "lexical-core", @@ -1576,18 +1485,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.120" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.120" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca2a8cb5805ce9e3b95435e3765b7b553cecc762d938d409434338386cb5775" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" dependencies = [ "proc-macro2", "quote", @@ -1738,17 +1647,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "syn" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cb8b1b4ebf86a89ee88cbd201b022b94138c623644d035185c84d3f41b7e66" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" dependencies = [ "proc-macro2", "quote", @@ -1783,9 +1686,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301bdd13d23c49672926be451130892d274d3ba0b410c18e00daa7990ff38d99" +checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915" dependencies = [ "once_cell", ] @@ -1801,9 +1704,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273d3ed44dca264b0d6b3665e8d48fb515042d42466fad93d2a45b90ec4058f7" +checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" dependencies = [ "const_fn", "libc", @@ -1839,9 +1742,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" dependencies = [ "tinyvec_macros", ] @@ -1854,9 +1757,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", "futures-core", @@ -2062,9 +1965,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2072,9 +1975,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" dependencies = [ "bumpalo", "lazy_static", @@ -2087,9 +1990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2097,9 +2000,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" dependencies = [ "proc-macro2", "quote", @@ -2110,15 +2013,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.69" +version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" +checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" [[package]] name = "web-sys" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 63a3582..63b1fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.2.8" +version = "0.2.9" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2018" @@ -12,11 +12,10 @@ base64 = "0.13" actix-web = { version = "3.3", features = ["rustls"] } futures = "0.3" askama = "0.10" -ureq = "2.0" +ureq = "2" serde = { version = "1.0", default_features = false, features = ["derive"] } serde_json = "1.0" async-recursion = "0.3" url = "2.2" regex = "1.4" -time = "0.2" -cached = "0.23.0" +time = "0.2" \ No newline at end of file diff --git a/README.md b/README.md index c3fda54..ee76d79 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > An alternative private front-end to Reddit -![screenshot](https://i.ibb.co/FxxbKM6/libreddit-rust.png) +![screenshot](https://i.ibb.co/F0JsY5K/image.png) --- @@ -40,7 +40,9 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) | [libreddit.dothq.co](https://libreddit.dothq.co) | 🇺🇸 US | ✅ | | [libreddit.insanity.wtf](https://libreddit.insanity.wtf) | 🇺🇸 US | ✅ | | [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ | +| [libreddit.himiko.cloud](https://libreddit.himiko.cloud) | 🇧🇬 BG | | | [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | | +| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | | 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. @@ -123,7 +125,7 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot 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 with troubleshooting. +**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs when Reddit is ratelimiting Libreddit and when Reddit's JSON responses can't be parsed. When debugging (running from source without `--release`), Libreddit logs post IDs and URL paths fetched to aid with troubleshooting. **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. diff --git a/src/main.rs b/src/main.rs index c705bbb..30b479a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,21 @@ async fn style() -> HttpResponse { HttpResponse::Ok().content_type("text/css").body(include_str!("../static/style.css")) } +// Required for creating a PWA +async fn manifest() -> HttpResponse { + HttpResponse::Ok().content_type("application/json").body(include_str!("../static/manifest.json")) +} + +// Required for the manifest to be valid +async fn pwa_logo() -> HttpResponse { + HttpResponse::Ok().content_type("image/png").body(include_bytes!("../static/logo.png").as_ref()) +} + +// Required for iOS App Icons +async fn iphone_logo() -> HttpResponse { + HttpResponse::Ok().content_type("image/png").body(include_bytes!("../static/touch-icon-iphone.png").as_ref()) +} + async fn robots() -> HttpResponse { HttpResponse::Ok() .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") @@ -27,6 +42,7 @@ async fn robots() -> HttpResponse { async fn favicon() -> HttpResponse { HttpResponse::Ok() + .content_type("image/x-icon") .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") .body(include_bytes!("../static/favicon.ico").as_ref()) } @@ -53,7 +69,7 @@ async fn main() -> std::io::Result<()> { .wrap_fn(move |req, srv| { let secure = req.connection_info().scheme() == "https"; let https_url = format!("https://{}{}", req.connection_info().host(), req.uri().to_string()); - srv.call(req).map(move |res: Result| + srv.call(req).map(move |res: Result| { if force_https && !secure { Ok(ServiceResponse::new( res.unwrap().request().to_owned(), @@ -62,16 +78,30 @@ async fn main() -> std::io::Result<()> { } else { res } - ) + }) }) // Append trailing slash and remove double slashes .wrap(middleware::NormalizePath::default()) + // Apply default headers for security + .wrap( + middleware::DefaultHeaders::new() + .header("Referrer-Policy", "no-referrer") + .header("X-Content-Type-Options", "nosniff") + .header("X-Frame-Options", "DENY") + .header( + "Content-Security-Policy", + "default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';", + ), + ) // Default service in case no routes match .default_service(web::get().to(|| utils::error("Nothing here".to_string()))) // Read static files .route("/style.css/", web::get().to(style)) .route("/favicon.ico/", web::get().to(favicon)) .route("/robots.txt/", web::get().to(robots)) + .route("/manifest.json/", web::get().to(manifest)) + .route("/logo.png/", web::get().to(pwa_logo)) + .route("/touch-icon-iphone.png/", web::get().to(iphone_logo)) // Proxy media through Libreddit .route("/proxy/{url:.*}/", web::get().to(proxy::handler)) // Browse user profile @@ -92,6 +122,8 @@ async fn main() -> std::io::Result<()> { // See posts and info about subreddit .route("/", web::get().to(subreddit::page)) .route("/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page)) + // Handle subscribe/unsubscribe + .route("/{action:subscribe|unsubscribe}/", web::post().to(subreddit::subscriptions)) // View post on subreddit .service( web::scope("/comments/{id}/{title}") diff --git a/src/post.rs b/src/post.rs index 7040de3..1b67d03 100644 --- a/src/post.rs +++ b/src/post.rs @@ -171,7 +171,11 @@ async fn parse_comments(json: &serde_json::Value) -> Vec { }, distinguished: val(&comment, "distinguished"), }, - score: format_num(score), + score: if comment["data"]["score_hidden"].as_bool().unwrap_or_default() { + "•".to_string() + } else { + format_num(score) + }, rel_time, created, replies, diff --git a/src/proxy.rs b/src/proxy.rs index bca2b0b..fcd571d 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -21,29 +21,27 @@ pub async fn handler(web::Path(b64): web::Path) -> Result "v.redd.it", ]; - match decode(b64) { - Ok(bytes) => { - let media = String::from_utf8(bytes).unwrap_or_default(); + let decoded = decode(b64).map(|bytes| String::from_utf8(bytes).unwrap_or_default()); - match Url::parse(media.as_str()) { - Ok(url) => { - let domain = url.domain().unwrap_or_default(); + match decoded { + Ok(media) => match Url::parse(media.as_str()) { + Ok(url) => { + let domain = url.domain().unwrap_or_default(); - if domains.contains(&domain) { - Client::default().get(media.replace("&", "&")).send().await.map_err(Error::from).map(|res| { - HttpResponse::build(res.status()) - .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") - .header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned()) - .header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned()) - .streaming(res) - }) - } else { - Err(error::ErrorForbidden("Resource must be from Reddit")) - } + if domains.contains(&domain) { + Client::default().get(media.replace("&", "&")).send().await.map_err(Error::from).map(|res| { + HttpResponse::build(res.status()) + .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") + .header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned()) + .header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned()) + .streaming(res) + }) + } else { + Err(error::ErrorForbidden("Resource must be from Reddit")) } - _ => Err(error::ErrorBadRequest("Can't parse base64 into URL")), } - } + _ => Err(error::ErrorBadRequest("Can't parse base64 into URL")), + }, _ => Err(error::ErrorBadRequest("Can't decode base64")), } } diff --git a/src/search.rs b/src/search.rs index dd8439d..a789d49 100644 --- a/src/search.rs +++ b/src/search.rs @@ -33,7 +33,7 @@ struct SearchTemplate { // SERVICES pub async fn find(req: HttpRequest) -> HttpResponse { - let nsfw_results = if cookie(&req, "hide_nsfw") != "on" { "&include_over_18=on" } else { "" }; + let nsfw_results = if cookie(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" }; let path = format!("{}.json?{}{}", req.path(), req.query_string(), nsfw_results); let sub = req.match_info().get("sub").unwrap_or("").to_string(); diff --git a/src/settings.rs b/src/settings.rs index 3b0fd72..6d7560f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -18,7 +18,7 @@ pub struct SettingsForm { layout: Option, wide: Option, comment_sort: Option, - hide_nsfw: Option, + show_nsfw: Option, } // FUNCTIONS @@ -33,8 +33,8 @@ pub async fn get(req: HttpRequest) -> HttpResponse { pub async fn set(_req: HttpRequest, form: Form) -> HttpResponse { let mut res = HttpResponse::Found(); - let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "hide_nsfw"]; - let values = vec![&form.theme, &form.front_page, &form.layout, &form.wide, &form.comment_sort, &form.hide_nsfw]; + let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw"]; + let values = vec![&form.theme, &form.front_page, &form.layout, &form.wide, &form.comment_sort, &form.show_nsfw]; for (i, name) in names.iter().enumerate() { match values[i] { diff --git a/src/subreddit.rs b/src/subreddit.rs index 8f4880a..7aac494 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -1,7 +1,8 @@ // CRATES use crate::utils::*; -use actix_web::{HttpRequest, HttpResponse, Result}; +use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result}; use askama::Template; +use time::{Duration, OffsetDateTime}; // STRUCTS #[derive(Template)] @@ -25,23 +26,43 @@ struct WikiTemplate { // SERVICES pub async fn page(req: HttpRequest) -> HttpResponse { - let path = format!("{}.json?{}", req.path(), req.query_string()); - let default = cookie(&req, "front_page"); - let sub_name = req + let subscribed = cookie(&req, "subscriptions"); + let front_page = cookie(&req, "front_page"); + let sort = req.match_info().get("sort").unwrap_or("hot").to_string(); + + let sub = req .match_info() .get("sub") - .unwrap_or(if default.is_empty() { "popular" } else { default.as_str() }) - .to_string(); - let sort = req.match_info().get("sort").unwrap_or("hot").to_string(); + .map(String::from) + .unwrap_or(if front_page == "default" || front_page.is_empty() { + if subscribed.is_empty() { + "popular".to_string() + } else { + subscribed.to_owned() + } + } else { + front_page.to_owned() + }); + + let path = format!("/r/{}/{}.json?{}", sub, sort, req.query_string()); match fetch_posts(&path, String::new()).await { Ok((posts, after)) => { // If you can get subreddit posts, also request subreddit metadata - let sub = if !sub_name.contains('+') && sub_name != "popular" && sub_name != "all" { - subreddit(&sub_name).await.unwrap_or_default() - } else if sub_name.contains('+') { + let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" { + // Regular subreddit + subreddit(&sub).await.unwrap_or_default() + } else if sub == subscribed { + // Subscription feed + if req.path().starts_with("/r/") { + subreddit(&sub).await.unwrap_or_default() + } else { + Subreddit::default() + } + } else if sub.contains('+') { + // Multireddit Subreddit { - name: sub_name, + name: sub, ..Subreddit::default() } } else { @@ -63,6 +84,50 @@ pub async fn page(req: HttpRequest) -> HttpResponse { } } +// Sub or unsub by setting subscription cookie using response "Set-Cookie" header +pub async fn subscriptions(req: HttpRequest) -> HttpResponse { + let mut res = HttpResponse::Found(); + + let sub = req.match_info().get("sub").unwrap_or_default().to_string(); + let action = req.match_info().get("action").unwrap_or_default().to_string(); + let mut sub_list = prefs(req.to_owned()).subs; + + // Modify sub list based on action + if action == "subscribe" && !sub_list.contains(&sub) { + sub_list.push(sub.to_owned()); + sub_list.sort(); + } else if action == "unsubscribe" { + sub_list.retain(|s| s != &sub); + } + + // Delete cookie if empty, else set + if sub_list.is_empty() { + res.del_cookie(&Cookie::build("subscriptions", "").path("/").finish()); + } else { + res.cookie( + Cookie::build("subscriptions", sub_list.join("+")) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .finish(), + ); + } + + // Redirect back to subreddit + // check for redirect parameter if unsubscribing from outside sidebar + let redirect_path = param(&req.uri().to_string(), "redirect"); + let path = if !redirect_path.is_empty() && redirect_path.starts_with('/') { + redirect_path + } else { + format!("/r/{}", sub) + }; + + res + .content_type("text/html") + .set_header("Location", path.to_owned()) + .body(format!("Redirecting to {0}...", path)) +} + pub async fn wiki(req: HttpRequest) -> HttpResponse { let sub = req.match_info().get("sub").unwrap_or("reddit.com").to_string(); let page = req.match_info().get("page").unwrap_or("index").to_string(); diff --git a/src/utils.rs b/src/utils.rs index 0c472f2..261af35 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use serde_json::{from_str, Value}; use std::collections::HashMap; use time::{Duration, OffsetDateTime}; use url::Url; -use cached::proc_macro::cached; +// use cached::proc_macro::cached; // // STRUCTS @@ -127,8 +127,9 @@ pub struct Preferences { pub front_page: String, pub layout: String, pub wide: String, - pub hide_nsfw: String, + pub show_nsfw: String, pub comment_sort: String, + pub subs: Vec, } // @@ -142,8 +143,9 @@ pub fn prefs(req: HttpRequest) -> Preferences { front_page: cookie(&req, "front_page"), layout: cookie(&req, "layout"), wide: cookie(&req, "wide"), - hide_nsfw: cookie(&req, "hide_nsfw"), + show_nsfw: cookie(&req, "show_nsfw"), comment_sort: cookie(&req, "comment_sort"), + subs: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), } } @@ -341,7 +343,11 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec HttpResponse { } // Make a request to a Reddit API and parse the JSON response -#[cached(size=1000,time=60, result = true)] +// #[cached(size=100,time=60, result = true)] pub async fn request(path: String) -> Result { let url = format!("https://www.reddit.com{}", path); let user_agent = format!("web:libreddit:{}", env!("CARGO_PKG_VERSION")); @@ -459,11 +465,11 @@ pub async fn request(path: String) -> Result { // If response is success Ok(response) => { // Parse the response from Reddit as JSON - match from_str(&response.into_string().unwrap()) { + let json_string = &response.into_string().unwrap_or_default(); + match from_str(json_string) { Ok(json) => Ok(json), - Err(_) => { - #[cfg(debug_assertions)] - dbg!(format!("{} - Failed to parse page JSON data", url)); + Err(e) => { + println!("{} - Failed to parse page JSON data: {} - {}", url, e, json_string); Err("Failed to parse page JSON data".to_string()) } } @@ -475,9 +481,8 @@ pub async fn request(path: String) -> Result { Err("Page not found".to_string()) } // If failed to send request - Err(_e) => { - #[cfg(debug_assertions)] - dbg!(format!("{} - {}", url, _e)); + Err(e) => { + println!("{} - Couldn't send request to Reddit: {}", url, e); Err("Couldn't send request to Reddit, this instance may be being rate-limited. Try another.".to_string()) } } diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000..bd005fc Binary files /dev/null and b/static/logo.png differ diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..4f02724 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "Libreddit", + "short_name": "Libreddit", + "display": "standalone", + "background_color": "#2A3443", + "description": "An alternative private front-end to Reddit", + "theme_color": "#2A3443", + "icons": [ + { + "src": "./logo.png", + "sizes": "512x512", + "type": "image/png" + } + ] + } \ No newline at end of file diff --git a/static/style.css b/static/style.css index 6c2f914..84662f2 100644 --- a/static/style.css +++ b/static/style.css @@ -58,6 +58,11 @@ background: var(--accent); } +:focus { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote, pre, form, fieldset, table, th, td, select, input { margin: 0; @@ -68,11 +73,12 @@ pre, form, fieldset, table, th, td, select, input { body { background: var(--background); font-size: 15px; + padding-top: 60px; } nav { display: grid; - grid-template-areas: "logo searchbox code"; + grid-template-areas: "logo searchbox links"; justify-content: space-between; align-items: center; @@ -83,7 +89,7 @@ nav { font-size: 20px; - z-index: 1; + z-index: 2; top: 0; padding: 5px 15px; min-height: 40px; @@ -92,23 +98,49 @@ nav { } nav * { color: var(--text); } -nav #reddit, #code { color: var(--accent); } -nav #logo { grid-area: logo; } -nav #code { grid-area: code; } -nav #version { opacity: 50%; } +nav #reddit, #code > span { color: var(--accent); } +nav #code > svg { stroke: var(--accent); } + +nav #logo { + grid-area: logo; + white-space: nowrap; + margin-right: 5px; +} + +nav #links { + grid-area: links; + margin-left: 10px; + display: flex; +} + +nav #links svg { + display: none; +} + +nav #version { + opacity: 50%; + vertical-align: -2px; + margin-right: 10px; +} + +nav #libreddit { + vertical-align: -2px; +} #settings_link { - font-size: 18px; - margin-left: 20px; opacity: 0.8; } +#code { + margin-left: 5px; +} + main { display: flex; justify-content: center; max-width: 1000px; padding: 10px 20px; - margin: 60px auto 20px auto + margin: 0 auto; } .wide main { @@ -232,6 +264,71 @@ aside { color: var(--accent); } +/* Subscriptions */ + +#sub_subscription { + margin-top: 20px; +} + +.subscribe, .unsubscribe { + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.subscribe { + color: var(--foreground); + background-color: var(--accent); +} + +.unsubscribe { + color: var(--text); + background-color: var(--highlighted); +} + +/* Subscribed subreddit list */ + +#subscriptions { + position: relative; + border-radius: 5px; + border: var(--panel-border); + background-color: var(--outside); + align-items: center; + box-sizing: border-box; + font-size: 15px; + display: inline-block; +} + +#subscriptions > summary { + padding: 8px 15px; +} + +#sub_list { + position: absolute; + display: flex; + min-width: 100%; + border-radius: 5px; + box-shadow: var(--shadow); + background: var(--outside); + flex-direction: column; + overflow: auto; + z-index: 1; +} + +#sub_list > a { + padding: 10px 20px; + transition: 0.2s background; +} + +#sub_list > .selected { + background-color: var(--accent); + color: var(--foreground); +} + +#sub_list > a:not(.selected):hover { + background-color: var(--foreground); +} + /* Wiki Pages */ #wiki { @@ -304,6 +401,7 @@ select, #search { align-items: center; border-right: 2px var(--outside) solid; padding: 0 10px; + max-width: 50%; } #restrict_sr { margin-right: 5px; } @@ -451,10 +549,6 @@ a.search_subreddit:hover { .post:not(:last-child) { margin-bottom: 10px; } -.post.highlighted { - margin: 20px 0; -} - .post:hover { background: var(--foreground); } @@ -788,7 +882,7 @@ a.search_subreddit:hover { /* Settings */ -#settings { +#settings, #settings > form { display: flex; flex-direction: column; align-items: center; @@ -801,7 +895,7 @@ a.search_subreddit:hover { opacity: 0.75; } -#prefs { +.prefs { display: flex; flex-direction: column; justify-content: space-between; @@ -811,7 +905,7 @@ a.search_subreddit:hover { border-radius: 5px; } -#prefs > div { +.prefs > div { display: flex; justify-content: space-between; width: 100%; @@ -819,17 +913,21 @@ a.search_subreddit:hover { align-items: center; } -#prefs > div:not(:last-of-type) { +.prefs > div:not(:last-of-type) { margin-bottom: 10px; } -#prefs select { +.prefs select { border-radius: 5px; box-shadow: var(--shadow); margin-left: 20px; background: var(--foreground); } +aside.prefs { + margin-top: 20px; +} + #save { background: var(--highlighted); padding: 10px 15px; @@ -842,6 +940,27 @@ input[type="submit"] { -webkit-appearance: none; -moz-appearance: none; } + +#settings_subs { + list-style: none; + padding: 0; +} + +#settings_subs > li { + display: flex; + margin: 10px 0; +} +#settings_subs > li:last-of-type { margin-bottom: 0; } + +#settings_subs > li > span { + padding: 10px 0; + margin-right: auto; +} + +#settings_subs .unsubscribe { + margin-left: 30px; +} + /* Markdown */ .md > *:not(:first-child) { @@ -915,6 +1034,8 @@ td, th { /* Mobile */ @media screen and (max-width: 480px) { + #version { display: none; } + .post { grid-template: "post_header post_header post_thumbnail" auto "post_title post_title post_thumbnail" 1fr @@ -953,25 +1074,39 @@ td, th { } @media screen and (max-width: 800px) { + body { padding-top: 120px } + main { flex-direction: column-reverse; padding: 10px; - margin: 100px 0 10px 0; + margin: 0 0 10px 0; max-width: 100%; } nav { - grid-template-areas: 'logo code' 'searchbox searchbox'; + grid-template-areas: 'logo links' 'searchbox searchbox'; padding: 10px; width: calc(100% - 20px); } + nav #links { margin-left: auto; } + nav #links span { display: none; } + nav #links svg { display: block; } + + #subscriptions { position: unset; } + + #sub_list { + left: 10px; + right: 10px; + min-width: auto; + } + aside, #subreddit, #user { margin: 0; max-width: 100%; } #user, #sidebar { margin: 20px 0; } - #logo { margin: 5px auto; } - #searchbox { width: 100%; } + #logo, #links { margin-bottom: 5px; } + #searchbox { width: calc(100vw - 35px); } } diff --git a/static/touch-icon-iphone.png b/static/touch-icon-iphone.png new file mode 100644 index 0000000..1b9b4d5 Binary files /dev/null and b/static/touch-icon-iphone.png differ diff --git a/templates/base.html b/templates/base.html index 0de8a8e..6400ebd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,13 +3,24 @@ {% block head %} {% block title %}Libreddit{% endblock %} - - + + + + + + + + + + + + + - + {% endblock %} diff --git a/templates/post.html b/templates/post.html index cdddaf2..619005e 100644 --- a/templates/post.html +++ b/templates/post.html @@ -13,6 +13,10 @@ {% endblock %} +{% block subscriptions %} + {% call utils::sub_list(post.community.as_str()) %} +{% endblock %} + {% macro comment(item) -%}
diff --git a/templates/search.html b/templates/search.html index ecbe340..1d6bb1e 100644 --- a/templates/search.html +++ b/templates/search.html @@ -3,6 +3,10 @@ {% block title %}Libreddit: search results - {{ params.q }}{% endblock %} +{% block subscriptions %} + {% call utils::sub_list("") %} +{% endblock %} + {% block content %}
@@ -42,7 +46,7 @@ {% endif %} {% for post in posts %} - {% if post.flags.nsfw && prefs.hide_nsfw == "on" %} + {% if post.flags.nsfw && prefs.show_nsfw != "on" %} {% else if post.title != "Comment" %}

diff --git a/templates/settings.html b/templates/settings.html index 0dbec00..8369ed8 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -8,45 +8,63 @@ {% endblock %} {% block content %} - -

-

Appearance

-
- - +
+ +
+

Appearance

+
+ + +
+

Interface

+
+ + +
+
+ + +
+
+ + +
+

Content

+
+ + +
+
+ + +
-

Interface

-
- - -
-
- - -
-
- - -
-

Content

-
- - -
-
- - -
-
-

Note: settings are saved in browser cookies. Clearing your cookie data will reset them.

- - +

Note: settings are saved in browser cookies. Clearing your cookie data will reset them.

+ + + {% if prefs.subs.len() > 0 %} + + {% endif %} +
+ {% endblock %} diff --git a/templates/subreddit.html b/templates/subreddit.html index 9a26aaa..6fd7325 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -11,6 +11,10 @@ {% call utils::search(["/r/", sub.name.as_str()].concat(), "") %} {% endblock %} +{% block subscriptions %} + {% call utils::sub_list(sub.name.as_str(), "wide") %} +{% endblock %} + {% block body %}
@@ -37,7 +41,7 @@
{% for post in posts %} - {% if !(post.flags.nsfw && prefs.hide_nsfw == "on") %} + {% if !(post.flags.nsfw && prefs.show_nsfw != "on") %}

@@ -121,6 +125,17 @@

{{ sub.members }}
{{ sub.active }}
+
+ {% if prefs.subs.contains(sub.name) %} +
+ +
+ {% else %} +
+ +
+ {% endif %} +