Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
64eec64ebe
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@ -1,2 +1,3 @@
|
||||
liberapay: spike
|
||||
custom: ['https://www.buymeacoffee.com/spikecodes']
|
||||
liberapay: sigaloid
|
||||
buy_me_a_coffee: sigaloid
|
||||
github: sigaloid
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE FILING A BUG REPORT: Ensure that you are running the latest git commit. Visit /info on your instance, and ensure the git commit listed is the same commit listed on the home page.
|
||||
-->
|
||||
|
||||
## Describe the bug
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
@ -31,3 +35,7 @@ Steps to reproduce the behavior:
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
||||
|
||||
<!-- Mandatory -->
|
||||
- [ ] I checked that the instance that this was reported on is running the latest git commit, or I can reproduce it locally on the latest git commit
|
175
Cargo.lock
generated
175
Cargo.lock
generated
@ -30,6 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
@ -78,44 +79,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.80"
|
||||
@ -190,9 +163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "6.0.0"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
|
||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@ -242,7 +215,7 @@ dependencies = [
|
||||
"cached_proc_macro",
|
||||
"cached_proc_macro_types",
|
||||
"futures",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"instant",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
@ -756,6 +729,12 @@ dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -865,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -877,6 +856,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.12"
|
||||
@ -920,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
@ -1069,6 +1054,18 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "once_map"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown 0.15.0",
|
||||
"parking_lot",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
@ -1227,7 +1224,7 @@ name = "redsunlib"
|
||||
version = "0.35.2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"askama",
|
||||
"async-recursion",
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
"build_html",
|
||||
@ -1246,12 +1243,14 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pretty_env_logger",
|
||||
"regex",
|
||||
"rinja",
|
||||
"route-recognizer",
|
||||
"rss",
|
||||
"rust-embed",
|
||||
"sealed_test",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_path",
|
||||
"serde_yaml",
|
||||
"time",
|
||||
"tokio",
|
||||
@ -1304,6 +1303,43 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"rinja_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja_derive"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"once_map",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rinja_parser",
|
||||
"rustc-hash",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja_parser"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
@ -1369,6 +1405,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
@ -1553,6 +1595,59 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_path_core",
|
||||
"serde_json_path_macros",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_macros"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde_json_path_core",
|
||||
"serde_json_path_macros_internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_macros_internal"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
@ -1626,6 +1721,12 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
|
@ -11,7 +11,7 @@ authors = [
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12.1", default-features = false }
|
||||
rinja = { version = "0.3.4", default-features = false }
|
||||
cached = { version = "0.51.3", features = ["async"] }
|
||||
clap = { version = "4.4.11", default-features = false, features = [
|
||||
"std",
|
||||
@ -31,7 +31,7 @@ time = { version = "0.3.31", features = ["local-offset"] }
|
||||
url = "2.5.0"
|
||||
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
||||
libflate = "2.0.0"
|
||||
brotli = { version = "6.0.0", features = ["std"] }
|
||||
brotli = { version = "7.0.0", features = ["std"] }
|
||||
toml = "0.8.8"
|
||||
once_cell = "1.19.0"
|
||||
serde_yaml = "0.9.29"
|
||||
@ -44,6 +44,8 @@ pretty_env_logger = "0.5.0"
|
||||
dotenvy = "0.15.7"
|
||||
rss = "2.0.7"
|
||||
arc-swap = "1.7.1"
|
||||
serde_json_path = "0.6.7"
|
||||
async-recursion = "1.1.1"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -71,7 +71,7 @@ And at the time, I was reading an excerpt from Mao Zedong, so the name seemed ap
|
||||
|
||||
- [Rust](https://www.rust-lang.org/) - Programming language
|
||||
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
|
||||
- [Askama](https://github.com/djc/askama) - Templating engine
|
||||
- [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
|
||||
- [Rustls](https://github.com/rustls/rustls) - TLS library
|
||||
|
||||
## How is it different from other Reddit front ends?
|
||||
@ -320,7 +320,7 @@ Assign a default value for each user-modifiable setting by passing environment v
|
||||
|
||||
| Name | Possible values | Default value |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
|
||||
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
|
||||
|
@ -14,6 +14,6 @@ PORT=12345
|
||||
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
||||
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)
|
||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=(sub1+sub2+sub3)
|
||||
#REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||
#REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
||||
|
@ -30,7 +30,8 @@ RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service ~@privileged ~@resources
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
|
118
src/client.rs
118
src/client.rs
@ -4,7 +4,6 @@ use futures_lite::future::block_on;
|
||||
use futures_lite::{future::Boxed, FutureExt};
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::StatusCode;
|
||||
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use libflate::gzip;
|
||||
@ -23,7 +22,13 @@ use crate::server::RequestExt;
|
||||
use crate::utils::format_url;
|
||||
|
||||
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||
const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
|
||||
|
||||
const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
|
||||
const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
|
||||
|
||||
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
|
||||
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
|
||||
|
||||
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||
@ -40,6 +45,11 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
|
||||
|
||||
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
static URL_PAIRS: [(&str, &str); 2] = [
|
||||
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
|
||||
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
|
||||
];
|
||||
|
||||
/// Gets the canonical path for a resource on Reddit. This is accomplished by
|
||||
/// making a `HEAD` request to Reddit at the path given in `path`.
|
||||
///
|
||||
@ -53,8 +63,28 @@ pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
||||
/// `Location` header. An `Err(String)` is returned if Reddit responds with a
|
||||
/// 429, or if we were unable to decode the value in the `Location` header.
|
||||
#[cached(size = 1024, time = 600, result = true)]
|
||||
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
let res = reddit_head(path.clone(), true).await?;
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
|
||||
if tries == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// for each URL pair, try the HEAD request
|
||||
let res = {
|
||||
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
|
||||
let mut res = None;
|
||||
for (url_base, url_base_host) in URL_PAIRS {
|
||||
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
|
||||
if let Some(res) = &res {
|
||||
if !res.status().is_client_error() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
};
|
||||
|
||||
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
|
||||
let status = res.status().as_u16();
|
||||
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
|
||||
|
||||
@ -68,6 +98,7 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
let Ok(original) = val.to_str() else {
|
||||
return Err("Unable to decode Location header.".to_string());
|
||||
};
|
||||
|
||||
// We need to strip the .json suffix from the original path.
|
||||
// In addition, we want to remove share parameters.
|
||||
// Cut it off here instead of letting it propagate all the way
|
||||
@ -80,7 +111,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
// also remove all Reddit domain parts with format_url.
|
||||
// Otherwise, it will literally redirect to Reddit.com.
|
||||
let uri = format_url(stripped_uri);
|
||||
Ok(Some(uri))
|
||||
|
||||
// Decrement tries and try again
|
||||
canonical_path(uri, tries - 1).await
|
||||
}
|
||||
None => Ok(None),
|
||||
},
|
||||
@ -161,20 +194,26 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
|
||||
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
|
||||
/// 3xx codes Reddit returns and will automatically redirect.
|
||||
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::GET, path, true, quarantine)
|
||||
request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
|
||||
}
|
||||
|
||||
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
||||
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::HEAD, path, false, quarantine)
|
||||
/// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
|
||||
fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::HEAD, path, false, quarantine, base_path, host)
|
||||
}
|
||||
|
||||
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
||||
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
// request(&Method::HEAD, path, false, quarantine, false)
|
||||
// }
|
||||
// Unused - reddit_head is only ever called in the context of a short URL
|
||||
|
||||
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
|
||||
/// will recurse on the URL that Reddit provides in the Location HTTP header
|
||||
/// in its response.
|
||||
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
||||
// Build Reddit URL from path.
|
||||
let url = format!("{REDDIT_URL_BASE}{path}");
|
||||
let url = format!("{base_path}{path}");
|
||||
|
||||
// Construct the hyper client from the HTTPS connector.
|
||||
let client: Client<_, Body> = CLIENT.clone();
|
||||
@ -199,7 +238,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
.header("Client-Vendor-Id", vendor_id)
|
||||
.header("X-Reddit-Device-Id", device_id)
|
||||
.header("x-reddit-loid", loid)
|
||||
.header("Host", "oauth.reddit.com")
|
||||
.header("Host", host)
|
||||
.header("Authorization", &format!("Bearer {token}"))
|
||||
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
@ -254,16 +293,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
.to_string(),
|
||||
true,
|
||||
quarantine,
|
||||
base_path,
|
||||
host,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
|
||||
if response.status() == StatusCode::FORBIDDEN && response.headers().get("retry-after").unwrap_or(&HeaderValue::from_static("0")).to_str().unwrap_or("0") == "0" {
|
||||
force_refresh_token().await;
|
||||
return Err("Rate limit - try refreshing soon".to_string());
|
||||
}
|
||||
|
||||
match response.headers().get(header::CONTENT_ENCODING) {
|
||||
// Content not compressed.
|
||||
None => Ok(response),
|
||||
@ -381,6 +416,16 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
match serde_json::from_reader(body.reader()) {
|
||||
Ok(value) => {
|
||||
let json: Value = value;
|
||||
|
||||
// If user is suspended
|
||||
if let Some(data) = json.get("data") {
|
||||
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
|
||||
if is_suspended {
|
||||
return Err("suspended".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If Reddit returned an error
|
||||
if json["error"].is_i64() {
|
||||
// OAuth token has expired; http status 401
|
||||
@ -389,6 +434,7 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
let () = force_refresh_token().await;
|
||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
||||
}
|
||||
|
||||
// Handle quarantined
|
||||
if json["reason"] == "quarantined" {
|
||||
return Err("quarantined".into());
|
||||
@ -397,6 +443,15 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
if json["reason"] == "gated" {
|
||||
return Err("gated".into());
|
||||
}
|
||||
// Handle private subs
|
||||
if json["reason"] == "private" {
|
||||
return Err("private".into());
|
||||
}
|
||||
// Handle banned subs
|
||||
if json["reason"] == "banned" {
|
||||
return Err("banned".into());
|
||||
}
|
||||
|
||||
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
|
||||
} else {
|
||||
Ok(json)
|
||||
@ -432,13 +487,34 @@ async fn test_localization_popular() {
|
||||
async fn test_obfuscated_share_link() {
|
||||
let share_link = "/r/rust/s/kPgq8WNHRK".into();
|
||||
// Correct link without share parameters
|
||||
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into();
|
||||
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link)));
|
||||
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
|
||||
assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_share_link_strip_json() {
|
||||
let link = "/17krzvz".into();
|
||||
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into();
|
||||
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link)));
|
||||
let canonical_link = "/comments/17krzvz".into();
|
||||
assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_private_sub() {
|
||||
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("private".into()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_banned_sub() {
|
||||
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("banned".into()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_gated_sub() {
|
||||
// quarantine to false to specifically catch when we _don't_ catch it
|
||||
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("gated".into()));
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ use crate::server::RequestExt;
|
||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
|
||||
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use serde_json::Value;
|
||||
use std::borrow::ToOwned;
|
||||
use std::collections::HashSet;
|
||||
|
@ -3,10 +3,10 @@ use crate::{
|
||||
server::RequestExt,
|
||||
utils::{ErrorTemplate, Preferences},
|
||||
};
|
||||
use askama::Template;
|
||||
use build_html::{Container, Html, HtmlContainer, Table};
|
||||
use hyper::{http::Error, Body, Request, Response};
|
||||
use once_cell::sync::Lazy;
|
||||
use rinja::Template;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
|
@ -284,6 +284,9 @@ async fn main() {
|
||||
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||
app
|
||||
.at("/emote/:subreddit_id/:filename")
|
||||
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
|
||||
app
|
||||
.at("/preview/:loc/award_images/:fullname/:id")
|
||||
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
|
||||
@ -389,7 +392,7 @@ async fn main() {
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
match req.param("id").as_deref() {
|
||||
// Share link
|
||||
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).await {
|
||||
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
|
||||
Ok(Some(path)) => Ok(redirect(&path)),
|
||||
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||
Err(e) => error(req, &e).await,
|
||||
@ -408,7 +411,7 @@ async fn main() {
|
||||
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
|
||||
|
||||
// Short link for post
|
||||
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}")).await {
|
||||
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
|
||||
Ok(path_opt) => match path_opt {
|
||||
Some(path) => Ok(redirect(&path)),
|
||||
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||
|
44
src/oauth.rs
44
src/oauth.rs
@ -6,13 +6,14 @@ use crate::{
|
||||
};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hyper::{client, Body, Method, Request};
|
||||
use log::{info, trace};
|
||||
use log::{error, info, trace};
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::time::{error::Elapsed, timeout};
|
||||
|
||||
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
|
||||
|
||||
static AUTH_ENDPOINT: &str = "https://accounts.reddit.com";
|
||||
static AUTH_ENDPOINT: &str = "https://www.reddit.com";
|
||||
|
||||
// Spoofed client for Android devices
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@ -25,11 +26,32 @@ pub struct Oauth {
|
||||
}
|
||||
|
||||
impl Oauth {
|
||||
/// Create a new OAuth client
|
||||
pub(crate) async fn new() -> Self {
|
||||
let mut oauth = Self::default();
|
||||
oauth.login().await;
|
||||
oauth
|
||||
// Call new_internal until it succeeds
|
||||
loop {
|
||||
let attempt = Self::new_with_timeout().await;
|
||||
match attempt {
|
||||
Ok(Some(oauth)) => {
|
||||
info!("[✅] Successfully created OAuth client");
|
||||
return oauth;
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Failed to create OAuth client. Retrying in 5 seconds...");
|
||||
continue;
|
||||
}
|
||||
Err(duration) => {
|
||||
error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_with_timeout() -> Result<Option<Self>, Elapsed> {
|
||||
let mut oauth = Self::default();
|
||||
timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth))
|
||||
}
|
||||
|
||||
pub(crate) fn default() -> Self {
|
||||
// Generate a device to spoof
|
||||
let device = Device::new();
|
||||
@ -46,7 +68,7 @@ impl Oauth {
|
||||
}
|
||||
async fn login(&mut self) -> Option<()> {
|
||||
// Construct URL for OAuth token
|
||||
let url = format!("{AUTH_ENDPOINT}/api/access_token");
|
||||
let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid");
|
||||
let mut builder = Request::builder().method(Method::POST).uri(&url);
|
||||
|
||||
// Add headers from spoofed client
|
||||
@ -69,13 +91,19 @@ impl Oauth {
|
||||
// Build request
|
||||
let request = builder.body(body).unwrap();
|
||||
|
||||
trace!("Sending token request...");
|
||||
|
||||
// Send request
|
||||
let client: client::Client<_, Body> = CLIENT.clone();
|
||||
let resp = client.request(request).await.ok()?;
|
||||
|
||||
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));
|
||||
|
||||
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
|
||||
// Technically it's not needed, but it's easy for Reddit API to check for this.
|
||||
// It's some kind of header that uniquely identifies the device.
|
||||
// Not worried about the privacy implications, since this is randomly changed
|
||||
// and really only as privacy-concerning as the OAuth token itself.
|
||||
if let Some(header) = resp.headers().get("x-reddit-loid") {
|
||||
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
|
||||
}
|
||||
@ -85,10 +113,14 @@ impl Oauth {
|
||||
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
|
||||
}
|
||||
|
||||
trace!("Serializing response...");
|
||||
|
||||
// Serialize response
|
||||
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
|
||||
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
|
||||
|
||||
trace!("Accessing relevant fields...");
|
||||
|
||||
// Save token and expiry
|
||||
self.token = json.get("access_token")?.as_str()?.to_string();
|
||||
self.expires_in = json.get("expires_in")?.as_u64()?;
|
||||
|
14
src/post.rs
14
src/post.rs
@ -4,14 +4,14 @@ use crate::config::get_setting;
|
||||
use crate::server::RequestExt;
|
||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||
use crate::utils::{
|
||||
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
||||
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_emotes, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
||||
};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use askama::Template;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use rinja::Template;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -72,11 +72,15 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
|
||||
}
|
||||
|
||||
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||
let query_body = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let query_string = format!("q={query_body}&type=comment");
|
||||
let form = url::form_urlencoded::parse(query_string.as_bytes()).collect::<HashMap<_, _>>();
|
||||
let query = form.get("q").unwrap().clone().to_string();
|
||||
|
||||
let comments = match query.as_str() {
|
||||
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
|
||||
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
|
||||
@ -174,7 +178,7 @@ fn build_comment(
|
||||
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
||||
)
|
||||
} else {
|
||||
rewrite_urls(&val(comment, "body_html"))
|
||||
rewrite_emotes(&data["media_metadata"], val(comment, "body_html"))
|
||||
};
|
||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||
|
||||
|
@ -5,10 +5,10 @@ use crate::{
|
||||
subreddit::{can_access_quarantine, quarantine},
|
||||
RequestExt,
|
||||
};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rinja::Template;
|
||||
|
||||
// STRUCTS
|
||||
struct SearchParams {
|
||||
@ -60,7 +60,8 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||
let uri_path = req.uri().path().replace("+", "%2B");
|
||||
let path = format!("{}.json?{}{}&raw_json=1", uri_path, req.uri().query().unwrap_or_default(), nsfw_results);
|
||||
let mut query = param(&path, "q").unwrap_or_default();
|
||||
query = REDDIT_URL_MATCH.replace(&query, "").to_string();
|
||||
|
||||
@ -68,10 +69,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
return Ok(redirect("/"));
|
||||
}
|
||||
|
||||
if query.starts_with("r/") {
|
||||
if query.starts_with("r/") || query.starts_with("user/") {
|
||||
return Ok(redirect(&format!("/{query}")));
|
||||
}
|
||||
|
||||
if query.starts_with("u/") {
|
||||
return Ok(redirect(&format!("/user{}", &query[1..])));
|
||||
}
|
||||
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
let quarantined = can_access_quarantine(&req, &sub);
|
||||
// Handle random subreddits
|
||||
|
@ -3,10 +3,10 @@ use std::collections::HashMap;
|
||||
// CRATES
|
||||
use crate::server::ResponseExt;
|
||||
use crate::utils::{redirect, template, Preferences};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use futures_lite::StreamExt;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
|
@ -4,9 +4,9 @@ use crate::utils::{
|
||||
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
||||
};
|
||||
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@ -494,6 +494,11 @@ pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
link: Some(utils::get_post_url(&post)),
|
||||
author: Some(post.author.name),
|
||||
content: Some(rewrite_urls(&post.body)),
|
||||
description: Some(format!(
|
||||
"<a href='{}{}'>Comments</a>",
|
||||
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
|
||||
post.permalink
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
|
@ -3,8 +3,8 @@ use crate::client::json;
|
||||
use crate::server::RequestExt;
|
||||
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
|
||||
use crate::{config, utils};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use time::{macros::format_description, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
|
107
src/utils.rs
107
src/utils.rs
@ -4,14 +4,15 @@ use crate::config::{self, get_setting};
|
||||
// CRATES
|
||||
//
|
||||
use crate::{client::json, server::RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rinja::Template;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde_json::Value;
|
||||
use serde_json_path::{JsonPath, JsonPathExt};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
@ -937,12 +938,19 @@ pub fn rewrite_urls(input_text: &str) -> String {
|
||||
// Rewrite Reddit links to Redlib
|
||||
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
|
||||
.to_string();
|
||||
text1 = REDDIT_EMOJI_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
.replace("%5C", "")
|
||||
.replace("\\_", "_");
|
||||
|
||||
loop {
|
||||
if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
|
||||
break;
|
||||
} else {
|
||||
text1 = REDDIT_EMOJI_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
text1 = text1.replace("%5C", "").replace("\\_", "_");
|
||||
|
||||
// Rewrite external media previews to Redlib
|
||||
loop {
|
||||
@ -998,6 +1006,83 @@ pub fn rewrite_urls(input_text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
|
||||
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
|
||||
|
||||
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
|
||||
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
|
||||
|
||||
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
|
||||
/* Create the paths we'll use to look for our data inside the json.
|
||||
Because we don't know the name of any given emote we use a wildcard to parse them. */
|
||||
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
|
||||
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
|
||||
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
|
||||
|
||||
// Extract all of the results from those json paths
|
||||
let link_nodes = media_metadata.json_path(&link_path);
|
||||
let id_nodes = media_metadata.json_path(&id_path);
|
||||
|
||||
// Initialize our vectors
|
||||
let mut id_vec = Vec::new();
|
||||
let mut link_vec = Vec::new();
|
||||
|
||||
// Add the relevant data to each of our vectors so we can access it by number later
|
||||
for current_id in id_nodes {
|
||||
id_vec.push(current_id)
|
||||
}
|
||||
for current_link in link_nodes {
|
||||
link_vec.push(current_link)
|
||||
}
|
||||
|
||||
/* Set index to the length of link_vec.
|
||||
This is one larger than we'll actually be looking at, but we correct that later */
|
||||
let mut index = link_vec.len();
|
||||
|
||||
// Comment needs to be in scope for when we call rewrite_urls()
|
||||
let mut comment = comment;
|
||||
|
||||
/* Loop until index hits zero.
|
||||
This also prevents us from trying to do anything on an empty vector */
|
||||
while index != 0 {
|
||||
/* Subtract 1 from index to get the real index we should be looking at.
|
||||
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
|
||||
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
|
||||
index -= 1;
|
||||
|
||||
// Convert our current index in id_vec into a string so we can search through it with regex
|
||||
let current_id = id_vec[index].to_string();
|
||||
|
||||
/* The ID number can be multiple lengths, so we capture it with regex.
|
||||
We also want to only attempt anything when we get matches to avoid panicking */
|
||||
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(¤t_id) {
|
||||
// Format the ID to include the colons it has in the comment text
|
||||
let id = format!(":{}:", &id_capture[1]);
|
||||
|
||||
// Convert current link to string to search through it with the regex
|
||||
let link = link_vec[index].to_string();
|
||||
|
||||
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
|
||||
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
|
||||
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
|
||||
It's a good idea and makes everything look nicer, so we'll do the same. */
|
||||
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
|
||||
|
||||
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
|
||||
let to_replace_with = format!(
|
||||
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
|
||||
&link_capture[1]
|
||||
);
|
||||
|
||||
// Inside the comment replace the ID we found with the string that will embed the image
|
||||
comment = comment.replace(&id, &to_replace_with).to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Call rewrite_urls() to transform any other Reddit links
|
||||
rewrite_urls(&comment)
|
||||
}
|
||||
|
||||
// Format vote count to a string that will be displayed.
|
||||
// Append `m` and `k` for millions and thousands respectively, and
|
||||
// round to the nearest tenth.
|
||||
@ -1319,3 +1404,11 @@ fn test_url_path_basename() {
|
||||
// empty path
|
||||
assert_eq!(url_path_basename("/"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewriting_emotes() {
|
||||
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
|
||||
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
|
||||
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
|
||||
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
:root,
|
||||
.dark {
|
||||
/* Default & fallback theme (dark) */
|
||||
--accent: aqua;
|
||||
--accent: #d54455;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
@ -62,7 +62,7 @@
|
||||
/* Browser-defined light theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--accent: #009a9a;
|
||||
--accent: #bb2b3b;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
@ -192,7 +192,7 @@ nav.fixed_navbar {
|
||||
nav * {
|
||||
color: var(--text);
|
||||
}
|
||||
nav #reddit,
|
||||
nav #red,
|
||||
#code > span {
|
||||
color: var(--accent);
|
||||
}
|
||||
@ -616,6 +616,7 @@ aside {
|
||||
#feed_list > .selected {
|
||||
background-color: var(--accent);
|
||||
color: var(--foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#feed_list > a:not(.selected):hover {
|
||||
@ -870,6 +871,7 @@ main > * > footer > a {
|
||||
#listing_options > a.selected {
|
||||
background: var(--accent);
|
||||
color: var(--foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#sort_options > a:not(.selected):hover,
|
||||
@ -1242,6 +1244,10 @@ a.search_subreddit:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.highlighted .post_poll {
|
||||
padding: 15px 0 5px;
|
||||
}
|
||||
|
||||
/* Used only for text post preview */
|
||||
.post_preview {
|
||||
-webkit-mask-image: linear-gradient(180deg, #000 60%, transparent);
|
||||
@ -1818,7 +1824,7 @@ input[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md > p:not(:first-child):not(:last-child) {
|
||||
.md > p:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* Black theme setting */
|
||||
.black {
|
||||
--accent: #009a9a;
|
||||
--accent: #bb2b3b;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* Dark theme setting */
|
||||
.dark{
|
||||
--accent: aqua;
|
||||
--accent: #d54455;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
|
14
static/themes/libredditBlack.css
Normal file
14
static/themes/libredditBlack.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Libreddit black theme setting */
|
||||
.libredditBlack {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
--background: black;
|
||||
--outside: black;
|
||||
--post: black;
|
||||
--panel-border: 2px solid #0f0f0f;
|
||||
--highlighted: #0f0f0f;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/libredditDark.css
Normal file
14
static/themes/libredditDark.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Libreddit dark theme setting */
|
||||
.libredditDark{
|
||||
--accent: aqua;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #0f0f0f;
|
||||
--outside: #1f1f1f;
|
||||
--post: #161616;
|
||||
--panel-border: 1px solid #333;
|
||||
--highlighted: #333;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
19
static/themes/libredditLight.css
Normal file
19
static/themes/libredditLight.css
Normal file
@ -0,0 +1,19 @@
|
||||
/* Libreddit light theme setting */
|
||||
.libredditLight {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
--background: #ddd;
|
||||
--outside: #ececec;
|
||||
--post: #eee;
|
||||
--panel-border: 1px solid #ccc;
|
||||
--highlighted: white;
|
||||
--visited: #555;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html:has(> .libredditLight) {
|
||||
/* Hint color theme to browser for scrollbar */
|
||||
color-scheme: light;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/* Light theme setting */
|
||||
.light {
|
||||
--accent: #009a9a;
|
||||
--accent: #bb2b3b;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
|
@ -38,7 +38,7 @@
|
||||
<nav class="
|
||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||
<div id="logo">
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a>
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib.</span></a>
|
||||
{% block subscriptions %}{% endblock %}
|
||||
</div>
|
||||
{% block search %}{% endblock %}
|
||||
|
@ -61,7 +61,7 @@ body %}
|
||||
<summary class="comment_data">
|
||||
<a
|
||||
class="comment_link"
|
||||
href="{{ post.permalink }}"
|
||||
href="{{ post.permalink }}#{{ post.id }}"
|
||||
title="{{ post.link_title }}"
|
||||
>{{ post.link_title }}</a
|
||||
>
|
||||
|
@ -157,7 +157,10 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- POST BODY -->
|
||||
<div class="post_body">{{ post.body|safe }}</div>
|
||||
<div class="post_body">
|
||||
{{ post.body|safe }}
|
||||
{% call poll(post) %}
|
||||
</div>
|
||||
<div class="post_score" title="{{ post.score.1 }}">
|
||||
{% if prefs.hide_score != "on" %}
|
||||
{{ post.score.0 }}
|
||||
|
Loading…
Reference in New Issue
Block a user