Merge branch 'main' into rss

This commit is contained in:
Matthew Esposito 2024-07-21 10:37:41 -04:00 committed by GitHub
commit 71d9d0ded4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1098 additions and 528 deletions

View file

@ -24,6 +24,8 @@ REDLIB_DEFAULT_WIDE=off
REDLIB_DEFAULT_POST_SORT=hot REDLIB_DEFAULT_POST_SORT=hot
# Set the default comment sort method (options: confidence, top, new, controversial, old) # Set the default comment sort method (options: confidence, top, new, controversial, old)
REDLIB_DEFAULT_COMMENT_SORT=confidence REDLIB_DEFAULT_COMMENT_SORT=confidence
# Enable blurring Spoiler content by default
REDLIB_DEFAULT_BLUR_SPOILER=off
# Enable showing NSFW content by default # Enable showing NSFW content by default
REDLIB_DEFAULT_SHOW_NSFW=off REDLIB_DEFAULT_SHOW_NSFW=off
# Enable blurring NSFW content by default # Enable blurring NSFW content by default
@ -36,8 +38,12 @@ REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3) # Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
REDLIB_DEFAULT_SUBSCRIPTIONS= REDLIB_DEFAULT_SUBSCRIPTIONS=
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
REDLIB_DEFAULT_FILTERS=
# Hide awards by default # Hide awards by default
REDLIB_DEFAULT_HIDE_AWARDS=off REDLIB_DEFAULT_HIDE_AWARDS=off
# Hide sidebar and summary
REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY=off
# Disable the confirmation before visiting Reddit # Disable the confirmation before visiting Reddit
REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
# Hide score by default # Hide score by default

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -7,6 +7,8 @@ on:
- "compose.*" - "compose.*"
branches: branches:
- "main" - "main"
release:
types: [published]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -60,7 +62,6 @@ jobs:
- name: Upload release - name: Upload release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: github.base_ref != 'main' && github.event_name == 'release'
with: with:
tag_name: ${{ steps.version.outputs.VERSION }} tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }} name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}

View file

@ -30,9 +30,15 @@ jobs:
with: with:
toolchain: stable toolchain: stable
- name: Install musl-gcc
run: sudo apt-get install musl-tools
- name: Install cargo musl target
run: rustup target add x86_64-unknown-linux-musl
# Building actions # Building actions
- name: Build - name: Build
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-musl
- name: Versions - name: Versions
id: version id: version
@ -45,17 +51,17 @@ jobs:
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Calculate SHA512 checksum - name: Calculate SHA512 checksum
run: sha512sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha512 run: sha512sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha512
- name: Calculate SHA256 checksum - name: Calculate SHA256 checksum
run: sha256sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha256 run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
name: Upload a Build Artifact name: Upload a Build Artifact
with: with:
name: redlib name: redlib
path: | path: |
target/x86_64-unknown-linux-gnu/release/redlib target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512 redlib.sha512
redlib.sha256 redlib.sha256
@ -68,7 +74,7 @@ jobs:
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }} name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
draft: true draft: true
files: | files: |
target/x86_64-unknown-linux-gnu/release/redlib target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512 redlib.sha512
redlib.sha256 redlib.sha256
body: | body: |

6
.gitignore vendored
View file

@ -1,4 +1,10 @@
/target /target
.env .env
redlib.toml
# Idea Files # Idea Files
.idea/ .idea/
# nix files
.direnv/
result

View file

@ -1,2 +1,2 @@
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-gnu/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done" run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-musl/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
language = "bash" language = "bash"

539
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ name = "redlib"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0" license = "AGPL-3.0"
repository = "https://github.com/redlib-org/redlib" repository = "https://github.com/redlib-org/redlib"
version = "0.31.2" version = "0.35.1"
authors = [ authors = [
"Matthew Esposito <matt+cargo@matthew.science>", "Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>", "spikecodes <19519553+spikecodes@users.noreply.github.com>",
@ -12,7 +12,7 @@ edition = "2021"
[dependencies] [dependencies]
askama = { version = "0.12.1", default-features = false } askama = { version = "0.12.1", default-features = false }
cached = { version = "0.48.1", features = ["async"] } cached = { version = "0.51.3", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [ clap = { version = "4.4.11", default-features = false, features = [
"std", "std",
"env", "env",
@ -31,18 +31,19 @@ time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0" url = "2.5.0"
rust-embed = { version = "8.1.0", features = ["include-exclude"] } rust-embed = { version = "8.1.0", features = ["include-exclude"] }
libflate = "2.0.0" libflate = "2.0.0"
brotli = { version = "3.4.0", features = ["std"] } brotli = { version = "6.0.0", features = ["std"] }
toml = "0.8.8" toml = "0.8.8"
once_cell = "1.19.0" once_cell = "1.19.0"
serde_yaml = "0.9.29" serde_yaml = "0.9.29"
build_html = "2.4.0" build_html = "2.4.0"
uuid = { version = "1.6.1", features = ["v4"] } uuid = { version = "1.6.1", features = ["v4"] }
base64 = "0.21.5" base64 = "0.22.1"
fastrand = "2.0.1" fastrand = "2.0.1"
log = "0.4.20" log = "0.4.20"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
rss = "2.0.7" rss = "2.0.7"
arc-swap = "1.7.1"
[dev-dependencies] [dev-dependencies]
lipsum = "0.9.0" lipsum = "0.9.0"

View file

@ -14,7 +14,7 @@ USER redlib
EXPOSE 8080 EXPOSE 8080
# Run a healthcheck every minute to make sure redlib is functional # Run a healthcheck every minute to make sure redlib is functional
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
CMD ["redlib"] CMD ["redlib"]

View file

@ -203,12 +203,6 @@ docker logs -f redlib
### Docker CLI ### Docker CLI
> [!IMPORTANT]
> If deploying on:
>
> - an `arm64` platform, use the `quay.io/redlib/redlib:latest-arm` image instead.
> - an `armv7` platform, use the `quay.io/redlib/redlib:latest-armv7` image instead.
Deploy Redlib: Deploy Redlib:
```bash ```bash
@ -380,12 +374,13 @@ REDLIB_DEFAULT_USE_HLS = "on"
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value | Description | | Name | Possible values | Default value | Description |
| ------------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------- | | ------------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. | | `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. | | `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. | | `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. | | `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
## Default user settings ## Default user settings
@ -393,12 +388,13 @@ Assign a default value for each user-modifiable setting by passing environment v
| Name | Possible values | Default value | | Name | Possible values | Default value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` | | `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` | | `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` | | `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` | | `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` | | `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` | | `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
| `SHOW_NSFW` | `["on", "off"]` | `off` | | `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` | | `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` | | `USE_HLS` | `["on", "off"]` | `off` |

View file

@ -29,6 +29,9 @@
"REDLIB_DEFAULT_POST_SORT": { "REDLIB_DEFAULT_POST_SORT": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_BLUR_SPOILER": {
"required": false
},
"REDLIB_DEFAULT_SHOW_NSFW": { "REDLIB_DEFAULT_SHOW_NSFW": {
"required": false "required": false
}, },
@ -59,6 +62,9 @@
"REDLIB_DEFAULT_SUBSCRIPTIONS": { "REDLIB_DEFAULT_SUBSCRIPTIONS": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_FILTERS": {
"required": false
},
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": { "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
"required": false "required": false
}, },

View file

@ -1,8 +1,6 @@
services: services:
redlib: redlib:
image: quay.io/redlib/redlib:latest image: quay.io/redlib/redlib:latest
# image: quay.io/redlib/redlib:latest-arm # uncomment if you use arm64
# image: quay.io/redlib/redlib:latest-armv7 # uncomment if you use armv7
restart: always restart: always
container_name: "redlib" container_name: "redlib"
ports: ports:

View file

@ -6,6 +6,7 @@ PORT=12345
#REDLIB_DEFAULT_WIDE=off #REDLIB_DEFAULT_WIDE=off
#REDLIB_DEFAULT_POST_SORT=hot #REDLIB_DEFAULT_POST_SORT=hot
#REDLIB_DEFAULT_COMMENT_SORT=confidence #REDLIB_DEFAULT_COMMENT_SORT=confidence
#REDLIB_DEFAULT_BLUR_SPOILER=off
#REDLIB_DEFAULT_SHOW_NSFW=off #REDLIB_DEFAULT_SHOW_NSFW=off
#REDLIB_DEFAULT_BLUR_NSFW=off #REDLIB_DEFAULT_BLUR_NSFW=off
#REDLIB_DEFAULT_USE_HLS=off #REDLIB_DEFAULT_USE_HLS=off

106
flake.lock generated Normal file
View file

@ -0,0 +1,106 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1717025063,
"narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=",
"owner": "ipetkov",
"repo": "crane",
"rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717112898,
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1717121863,
"narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

71
flake.nix Normal file
View file

@ -0,0 +1,71 @@
{
description = "Redlib: Private front-end for Reddit";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
};
outputs = { nixpkgs, crane, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
inherit (pkgs) lib;
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
targets = [ "x86_64-unknown-linux-musl" ];
};
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
src = lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(lib.hasInfix "/templates/" path) ||
(lib.hasInfix "/static/" path) ||
(craneLib.filterCargoSources path type);
};
redlib = craneLib.buildPackage {
inherit src;
strictDeps = true;
doCheck = false;
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
};
in
{
checks = {
my-crate = redlib;
};
packages.default = redlib;
packages.docker = pkgs.dockerTools.buildImage {
name = "quay.io/redlib/redlib";
tag = "latest";
created = "now";
copyToRoot = with pkgs.dockerTools; [ caCertificates fakeNss ];
config.Cmd = "${redlib}/bin/redlib";
};
});
}

31
scripts/load_test.py Normal file
View file

@ -0,0 +1,31 @@
import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
base_url = "http://localhost:8080"
full_path = f"{base_url}/r/politics"
ctr = 0
def fetch_url(url):
global ctr
response = requests.get(url)
ctr += 1
print(f"Request count: {ctr}")
return response
while full_path:
response = requests.get(full_path)
ctr += 1
print(f"Request count: {ctr}")
soup = BeautifulSoup(response.text, 'html.parser')
comment_links = soup.find_all('a', class_='post_comments')
comment_urls = [base_url + link['href'] for link in comment_links]
with ThreadPoolExecutor(max_workers=10) as executor:
executor.map(fetch_url, comment_urls)
next_link = soup.find('a', accesskey='N')
if next_link:
full_path = base_url + next_link['href']
else:
break

View file

@ -1,17 +1,20 @@
use arc_swap::ArcSwap;
use cached::proc_macro::cached; use cached::proc_macro::cached;
use futures_lite::future::block_on; use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt}; use futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::header::HeaderValue;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri}; use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector; use hyper_rustls::HttpsConnector;
use libflate::gzip; use libflate::gzip;
use log::error; use log::{error, trace, warn};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use percent_encoding::{percent_encode, CONTROLS}; use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value; use serde_json::Value;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicBool, AtomicU16};
use std::{io, result::Result}; use std::{io, result::Result};
use tokio::sync::RwLock;
use crate::dbg_msg; use crate::dbg_msg;
use crate::oauth::{force_refresh_token, token_daemon, Oauth}; use crate::oauth::{force_refresh_token, token_daemon, Oauth};
@ -19,6 +22,7 @@ use crate::server::RequestExt;
use crate::utils::format_url; use crate::utils::format_url;
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new() let https = hyper_rustls::HttpsConnectorBuilder::new()
@ -30,12 +34,16 @@ pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
client::Client::builder().build(https) client::Client::builder().build(https)
}); });
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| { pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new()); let client = block_on(Oauth::new());
tokio::spawn(token_daemon()); tokio::spawn(token_daemon());
RwLock::new(client) ArcSwap::new(client.into())
}); });
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
/// Gets the canonical path for a resource on Reddit. This is accomplished by /// 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`. /// making a `HEAD` request to Reddit at the path given in `path`.
/// ///
@ -171,7 +179,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
let client: Client<_, Body> = CLIENT.clone(); let client: Client<_, Body> = CLIENT.clone();
let (token, vendor_id, device_id, user_agent, loid) = { let (token, vendor_id, device_id, user_agent, loid) = {
let client = block_on(OAUTH_CLIENT.read()); let client = OAUTH_CLIENT.load_full();
( (
client.token.clone(), client.token.clone(),
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(), client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
@ -180,6 +188,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
) )
}; };
// Build request to Reddit. When making a GET, request gzip compression. // Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.) // (Reddit doesn't do brotli yet.)
let builder = Request::builder() let builder = Request::builder()
@ -214,12 +223,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
if !redirect { if !redirect {
return Ok(response); return Ok(response);
}; };
let location_header = response.headers().get(header::LOCATION);
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
return Err("Reddit response was invalid".to_string());
}
return request( return request(
method, method,
response location_header
.headers()
.get(header::LOCATION)
.map(|val| { .map(|val| {
// We need to make adjustments to the URI // We need to make adjustments to the URI
// we get back from Reddit. Namely, we // we get back from Reddit. Namely, we
@ -232,7 +242,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// required. // required.
// //
// 2. Percent-encode the path. // 2. Percent-encode the path.
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string(); let new_path = percent_encode(val.as_bytes(), CONTROLS)
.to_string()
.trim_start_matches(REDDIT_URL_BASE)
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
.to_string();
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" }) format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
}) })
.unwrap_or_default() .unwrap_or_default()
@ -291,7 +305,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
} }
} }
Err(e) => { Err(e) => {
dbg_msg!("{} {}: {}", method, path, e); dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
Err(e.to_string()) Err(e.to_string())
} }
@ -306,19 +320,56 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
#[cached(size = 100, time = 30, result = true)] #[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> { pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// Closure to quickly build errors // Closure to quickly build errors
let err = |msg: &str, e: String| -> Result<Value, String> { let err = |msg: &str, e: String, path: String| -> Result<Value, String> {
// eprintln!("{} - {}: {}", url, msg, e); // eprintln!("{} - {}: {}", url, msg, e);
Err(format!("{msg}: {e}")) Err(format!("{msg}: {e} | {path}"))
}; };
// First, handle rolling over the OAUTH_CLIENT if need be.
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
let is_rolling_over = OAUTH_IS_ROLLING_OVER.load(Ordering::SeqCst);
if current_rate_limit < 10 && !is_rolling_over {
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
tokio::spawn(force_refresh_token());
}
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
// Fetch the url... // Fetch the url...
match reddit_get(path.clone(), quarantine).await { match reddit_get(path.clone(), quarantine).await {
Ok(response) => { Ok(response) => {
let status = response.status(); let status = response.status();
let reset: Option<String> = if let (Some(remaining), Some(reset), Some(used)) = (
response.headers().get("x-ratelimit-remaining").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
) {
trace!(
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
if is_rolling_over { "yes" } else { "no" },
);
Some(reset)
} else {
None
};
// asynchronously aggregate the chunks of the body // asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await { match hyper::body::aggregate(response).await {
Ok(body) => { Ok(body) => {
let has_remaining = body.has_remaining();
if !has_remaining {
// Rate limited, so spawn a force_refresh_token()
tokio::spawn(force_refresh_token());
return match reset {
Some(val) => Err(format!(
"Reddit rate limit exceeded. Try refreshing in a few seconds.\
Rate limit will reset in: {val}"
)),
None => Err("Reddit rate limit exceeded".to_string()),
};
}
// Parse the response from Reddit as JSON // Parse the response from Reddit as JSON
match serde_json::from_reader(body.reader()) { match serde_json::from_reader(body.reader()) {
Ok(value) => { Ok(value) => {
@ -331,7 +382,7 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await; let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string()); return Err("OAuth token has expired. Please refresh the page!".to_string());
} }
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"])) Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else { } else {
Ok(json) Ok(json)
} }
@ -341,21 +392,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
if status.is_server_error() { if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string()) Err("Reddit is having issues, check if there's an outage".to_string())
} else { } else {
err("Failed to parse page JSON data", e.to_string()) err("Failed to parse page JSON data", e.to_string(), path)
} }
} }
} }
} }
Err(e) => err("Failed receiving body from Reddit", e.to_string()), Err(e) => err("Failed receiving body from Reddit", e.to_string(), path),
} }
} }
Err(e) => err("Couldn't send request to Reddit", e), Err(e) => err("Couldn't send request to Reddit", e, path),
} }
} }
#[cfg(test)]
static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL";
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_localization_popular() { async fn test_localization_popular() {
let val = json("/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL".to_string(), false).await.unwrap(); let val = json(POPULAR_URL.to_string(), false).await.unwrap();
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap()); assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
} }

View file

@ -48,6 +48,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")] #[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
pub(crate) default_post_sort: Option<String>, pub(crate) default_post_sort: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_BLUR_SPOILER")]
#[serde(alias = "LIBREDDIT_DEFAULT_BLUR_SPOILER")]
pub(crate) default_blur_spoiler: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")] #[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")] #[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
pub(crate) default_show_nsfw: Option<String>, pub(crate) default_show_nsfw: Option<String>,
@ -68,6 +72,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
pub(crate) default_hide_awards: Option<String>, pub(crate) default_hide_awards: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
pub(crate) default_hide_sidebar_and_summary: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")] #[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
pub(crate) default_hide_score: Option<String>, pub(crate) default_hide_score: Option<String>,
@ -76,6 +84,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")] #[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
pub(crate) default_subscriptions: Option<String>, pub(crate) default_subscriptions: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_FILTERS")]
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>, pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -122,13 +134,16 @@ impl Config {
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"), default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
default_wide: parse("REDLIB_DEFAULT_WIDE"), default_wide: parse("REDLIB_DEFAULT_WIDE"),
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"), default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
default_blur_spoiler: parse("REDLIB_DEFAULT_BLUR_SPOILER"),
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"), default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"), default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"), default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS"), default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"),
default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"), default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"),
default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"), default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"), default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"), default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
@ -145,14 +160,17 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(), "REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), "REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(), "REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
"REDLIB_DEFAULT_BLUR_SPOILER" => config.default_blur_spoiler.clone(),
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), "REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), "REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(), "REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(), "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
"REDLIB_DEFAULT_WIDE" => config.default_wide.clone(), "REDLIB_DEFAULT_WIDE" => config.default_wide.clone(),
"REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(), "REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
"REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(),
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(), "REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(), "REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(), "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(), "REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
@ -225,6 +243,12 @@ fn test_default_subscriptions() {
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into())); assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
} }
#[test]
#[sealed_test(env = [("REDLIB_DEFAULT_FILTERS", "news+bestof")])]
fn test_default_filters() {
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
}
#[test] #[test]
#[sealed_test] #[sealed_test]
fn test_pushshift() { fn test_pushshift() {

View file

@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
} }
if have_after { if have_after {
before = "t3_".to_owned(); "t3_".clone_into(&mut before);
before.push_str(&duplicates[0].id); before.push_str(&duplicates[0].id);
} }
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
if have_before { if have_before {
// The next batch will need to start from one after the // The next batch will need to start from one after the
// last post in the current batch. // last post in the current batch.
after = "t3_".to_owned(); "t3_".clone_into(&mut after);
after.push_str(&duplicates[l - 1].id); after.push_str(&duplicates[l - 1].id);
// Here is where things get terrible. Notice that we // Here is where things get terrible. Notice that we
@ -182,7 +182,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
match json(new_path, true).await { match json(new_path, true).await {
Ok(response) => { Ok(response) => {
if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() { if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() {
before = "t3_".to_owned(); "t3_".clone_into(&mut before);
before.push_str(&duplicates[0].id); before.push_str(&duplicates[0].id);
} }
} }

View file

@ -141,11 +141,13 @@ impl InstanceInfo {
["Wide", &convert(&self.config.default_wide)], ["Wide", &convert(&self.config.default_wide)],
["Comment sort", &convert(&self.config.default_comment_sort)], ["Comment sort", &convert(&self.config.default_comment_sort)],
["Post sort", &convert(&self.config.default_post_sort)], ["Post sort", &convert(&self.config.default_post_sort)],
["Blur Spoiler", &convert(&self.config.default_blur_spoiler)],
["Show NSFW", &convert(&self.config.default_show_nsfw)], ["Show NSFW", &convert(&self.config.default_show_nsfw)],
["Blur NSFW", &convert(&self.config.default_blur_nsfw)], ["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
["Use HLS", &convert(&self.config.default_use_hls)], ["Use HLS", &convert(&self.config.default_use_hls)],
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)], ["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)],
]) ])
.with_header_row(["Default preferences"]), .with_header_row(["Default preferences"]),
); );
@ -173,11 +175,13 @@ impl InstanceInfo {
Default wide: {:?}\n Default wide: {:?}\n
Default comment sort: {:?}\n Default comment sort: {:?}\n
Default post sort: {:?}\n Default post sort: {:?}\n
Default blur Spoiler: {:?}\n
Default show NSFW: {:?}\n Default show NSFW: {:?}\n
Default blur NSFW: {:?}\n Default blur NSFW: {:?}\n
Default use HLS: {:?}\n Default use HLS: {:?}\n
Default hide HLS notification: {:?}\n Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n", Default subscriptions: {:?}\n
Default filters: {:?}\n",
self.package_name, self.package_name,
self.crate_version, self.crate_version,
self.git_commit, self.git_commit,
@ -195,11 +199,13 @@ impl InstanceInfo {
self.config.default_wide, self.config.default_wide,
self.config.default_comment_sort, self.config.default_comment_sort,
self.config.default_post_sort, self.config.default_post_sort,
self.config.default_blur_spoiler,
self.config.default_show_nsfw, self.config.default_show_nsfw,
self.config.default_blur_nsfw, self.config.default_blur_nsfw,
self.config.default_use_hls, self.config.default_use_hls,
self.config.default_hide_hls_notification, self.config.default_hide_hls_notification,
self.config.default_subscriptions, self.config.default_subscriptions,
self.config.default_filters,
) )
} }
StringType::Html => self.to_table(), StringType::Html => self.to_table(),

View file

@ -135,7 +135,7 @@ async fn main() {
.long("address") .long("address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Sets address to listen on") .help("Sets address to listen on")
.default_value("0.0.0.0") .default_value("[::]")
.num_args(1), .num_args(1),
) )
.arg( .arg(

View file

@ -1,12 +1,12 @@
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
use crate::{ use crate::{
client::{CLIENT, OAUTH_CLIENT}, client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
oauth_resources::ANDROID_APP_VERSION_LIST, oauth_resources::ANDROID_APP_VERSION_LIST,
}; };
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use hyper::{client, Body, Method, Request}; use hyper::{client, Body, Method, Request};
use log::info; use log::{info, trace};
use serde_json::json; use serde_json::json;
@ -98,21 +98,13 @@ impl Oauth {
Some(()) Some(())
} }
async fn refresh(&mut self) -> Option<()> {
// Refresh is actually just a subsequent login with the same headers (without the old token
// or anything). This logic is handled in login, so we just call login again.
let refresh = self.login().await;
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
refresh
}
} }
pub async fn token_daemon() { pub async fn token_daemon() {
// Monitor for refreshing token // Monitor for refreshing token
loop { loop {
// Get expiry time - be sure to not hold the read lock // Get expiry time - be sure to not hold the read lock
let expires_in = { OAUTH_CLIENT.read().await.expires_in }; let expires_in = { OAUTH_CLIENT.load_full().expires_in };
// sleep for the expiry time minus 2 minutes // sleep for the expiry time minus 2 minutes
let duration = Duration::from_secs(expires_in - 120); let duration = Duration::from_secs(expires_in - 120);
@ -125,13 +117,22 @@ pub async fn token_daemon() {
// Refresh token - in its own scope // Refresh token - in its own scope
{ {
OAUTH_CLIENT.write().await.refresh().await; force_refresh_token().await;
} }
} }
} }
pub async fn force_refresh_token() { pub async fn force_refresh_token() {
OAUTH_CLIENT.write().await.refresh().await; if OAUTH_IS_ROLLING_OVER.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
trace!("Skipping refresh token roll over, already in progress");
return;
}
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
let new_client = Oauth::new().await;
OAUTH_CLIENT.swap(new_client.into());
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
OAUTH_IS_ROLLING_OVER.store(false, Ordering::SeqCst);
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -179,21 +180,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client() { async fn test_oauth_client() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty()); assert!(!OAUTH_CLIENT.load_full().token.is_empty());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client_refresh() { async fn test_oauth_client_refresh() {
OAUTH_CLIENT.write().await.refresh().await.unwrap(); force_refresh_token().await;
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_token_exists() { async fn test_oauth_token_exists() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty()); assert!(!OAUTH_CLIENT.load_full().token.is_empty());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_headers_len() { async fn test_oauth_headers_len() {
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3); assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
} }
#[test] #[test]

View file

@ -4,6 +4,44 @@
// Filled in with real app versions // Filled in with real app versions
pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.48.0/Build 1319123",
"Version 2023.49.0/Build 1321715",
"Version 2023.49.1/Build 1322281",
"Version 2023.50.0/Build 1332338",
"Version 2023.50.1/Build 1345844",
"Version 2024.02.0/Build 1368985",
"Version 2024.03.0/Build 1379408",
"Version 2024.04.0/Build 1391236",
"Version 2024.05.0/Build 1403584",
"Version 2024.06.0/Build 1418489",
"Version 2024.07.0/Build 1429651",
"Version 2024.08.0/Build 1439531",
"Version 2024.10.0/Build 1470045",
"Version 2024.10.1/Build 1478645",
"Version 2024.11.0/Build 1480707",
"Version 2024.12.0/Build 1494694",
"Version 2024.13.0/Build 1505187",
"Version 2024.14.0/Build 1520556",
"Version 2024.15.0/Build 1536823",
"Version 2024.16.0/Build 1551366",
"Version 2024.17.0/Build 1568106",
"Version 2024.18.0/Build 1577901",
"Version 2024.18.1/Build 1585304",
"Version 2024.19.0/Build 1593346",
"Version 2024.20.0/Build 1612800",
"Version 2024.20.1/Build 1615586",
"Version 2024.20.2/Build 1624969",
"Version 2024.21.0/Build 1631686",
"Version 2024.22.0/Build 1645257",
"Version 2024.22.1/Build 1652272",
"Version 2023.21.0/Build 956283",
"Version 2023.22.0/Build 968223",
"Version 2023.23.0/Build 983896",
"Version 2023.24.0/Build 998541",
"Version 2023.25.0/Build 1014750",
"Version 2023.25.1/Build 1018737",
"Version 2023.26.0/Build 1019073",
"Version 2023.27.0/Build 1031923",
"Version 2023.28.0/Build 1046887", "Version 2023.28.0/Build 1046887",
"Version 2023.29.0/Build 1059855", "Version 2023.29.0/Build 1059855",
"Version 2023.30.0/Build 1078734", "Version 2023.30.0/Build 1078734",
@ -26,14 +64,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.44.0/Build 1268622", "Version 2023.44.0/Build 1268622",
"Version 2023.45.0/Build 1281371", "Version 2023.45.0/Build 1281371",
"Version 2023.47.0/Build 1303604", "Version 2023.47.0/Build 1303604",
"Version 2023.48.0/Build 1319123", "Version 2022.42.0/Build 638508",
"Version 2023.49.0/Build 1321715", "Version 2022.43.0/Build 648277",
"Version 2023.49.1/Build 1322281", "Version 2022.44.0/Build 664348",
"Version 2023.50.0/Build 1332338", "Version 2022.45.0/Build 677985",
"Version 2023.50.1/Build 1345844", "Version 2023.01.0/Build 709875",
"Version 2024.02.0/Build 1368985", "Version 2023.02.0/Build 717912",
"Version 2024.03.0/Build 1379408", "Version 2023.03.0/Build 729220",
"Version 2024.04.0/Build 1391236", "Version 2023.04.0/Build 744681",
"Version 2023.05.0/Build 755453", "Version 2023.05.0/Build 755453",
"Version 2023.06.0/Build 775017", "Version 2023.06.0/Build 775017",
"Version 2023.07.0/Build 788827", "Version 2023.07.0/Build 788827",
@ -56,14 +94,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.19.0/Build 927681", "Version 2023.19.0/Build 927681",
"Version 2023.20.0/Build 943980", "Version 2023.20.0/Build 943980",
"Version 2023.20.1/Build 946732", "Version 2023.20.1/Build 946732",
"Version 2023.21.0/Build 956283", "Version 2022.20.0/Build 487703",
"Version 2023.22.0/Build 968223", "Version 2022.21.0/Build 492436",
"Version 2023.23.0/Build 983896", "Version 2022.22.0/Build 498700",
"Version 2023.24.0/Build 998541", "Version 2022.23.0/Build 502374",
"Version 2023.25.0/Build 1014750", "Version 2022.23.1/Build 506606",
"Version 2023.25.1/Build 1018737", "Version 2022.24.0/Build 510950",
"Version 2023.26.0/Build 1019073", "Version 2022.24.1/Build 513462",
"Version 2023.27.0/Build 1031923", "Version 2022.25.0/Build 515072",
"Version 2022.25.1/Build 516394", "Version 2022.25.1/Build 516394",
"Version 2022.25.2/Build 519915", "Version 2022.25.2/Build 519915",
"Version 2022.26.0/Build 521193", "Version 2022.26.0/Build 521193",
@ -86,14 +124,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.40.0/Build 624782", "Version 2022.40.0/Build 624782",
"Version 2022.41.0/Build 630468", "Version 2022.41.0/Build 630468",
"Version 2022.41.1/Build 634168", "Version 2022.41.1/Build 634168",
"Version 2022.42.0/Build 638508", "Version 2021.39.1/Build 372418",
"Version 2022.43.0/Build 648277", "Version 2021.41.0/Build 376052",
"Version 2022.44.0/Build 664348", "Version 2021.42.0/Build 378193",
"Version 2022.45.0/Build 677985", "Version 2021.43.0/Build 382019",
"Version 2023.01.0/Build 709875", "Version 2021.44.0/Build 385129",
"Version 2023.02.0/Build 717912", "Version 2021.45.0/Build 387663",
"Version 2023.03.0/Build 729220", "Version 2021.46.0/Build 392043",
"Version 2023.04.0/Build 744681", "Version 2021.47.0/Build 394342",
"Version 2022.10.0/Build 429896", "Version 2022.10.0/Build 429896",
"Version 2022.1.0/Build 402829", "Version 2022.1.0/Build 402829",
"Version 2022.11.0/Build 433004", "Version 2022.11.0/Build 433004",
@ -106,15 +144,7 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.17.0/Build 468480", "Version 2022.17.0/Build 468480",
"Version 2022.18.0/Build 473740", "Version 2022.18.0/Build 473740",
"Version 2022.19.1/Build 482464", "Version 2022.19.1/Build 482464",
"Version 2022.20.0/Build 487703",
"Version 2022.2.0/Build 405543", "Version 2022.2.0/Build 405543",
"Version 2022.21.0/Build 492436",
"Version 2022.22.0/Build 498700",
"Version 2022.23.0/Build 502374",
"Version 2022.23.1/Build 506606",
"Version 2022.24.0/Build 510950",
"Version 2022.24.1/Build 513462",
"Version 2022.25.0/Build 515072",
"Version 2022.3.0/Build 408637", "Version 2022.3.0/Build 408637",
"Version 2022.4.0/Build 411368", "Version 2022.4.0/Build 411368",
"Version 2022.5.0/Build 414731", "Version 2022.5.0/Build 414731",
@ -124,35 +154,5 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.7.0/Build 420849", "Version 2022.7.0/Build 420849",
"Version 2022.8.0/Build 423906", "Version 2022.8.0/Build 423906",
"Version 2022.9.0/Build 426592", "Version 2022.9.0/Build 426592",
"Version 2021.20.0/Build 326964",
"Version 2021.21.0/Build 327703",
"Version 2021.21.1/Build 328461",
"Version 2021.22.0/Build 329696",
"Version 2021.23.0/Build 331631",
"Version 2021.24.0/Build 333951",
"Version 2021.25.0/Build 335451",
"Version 2021.26.0/Build 336739",
"Version 2021.27.0/Build 338857",
"Version 2021.28.0/Build 340747",
"Version 2021.29.0/Build 342342",
"Version 2021.30.0/Build 343820",
"Version 2021.31.0/Build 346485",
"Version 2021.32.0/Build 349507",
"Version 2021.33.0/Build 351843",
"Version 2021.34.0/Build 353911",
"Version 2021.35.0/Build 355878",
"Version 2021.36.0/Build 359254",
"Version 2021.36.1/Build 360572",
"Version 2021.37.0/Build 361905",
"Version 2021.38.0/Build 365032",
"Version 2021.39.0/Build 369068",
"Version 2021.39.1/Build 372418",
"Version 2021.41.0/Build 376052",
"Version 2021.42.0/Build 378193",
"Version 2021.43.0/Build 382019",
"Version 2021.44.0/Build 385129",
"Version 2021.45.0/Build 387663",
"Version 2021.46.0/Build 392043",
"Version 2021.47.0/Build 394342",
]; ];
pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""];

View file

@ -1,3 +1,5 @@
#![allow(dead_code)]
use brotli::enc::{BrotliCompress, BrotliEncoderParams}; use brotli::enc::{BrotliCompress, BrotliEncoderParams};
use cached::proc_macro::cached; use cached::proc_macro::cached;
use cookie::Cookie; use cookie::Cookie;
@ -15,6 +17,7 @@ use libflate::gzip;
use route_recognizer::{Params, Router}; use route_recognizer::{Params, Router};
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
fmt::Display,
io, io,
pin::Pin, pin::Pin,
result::Result, result::Result,
@ -65,12 +68,12 @@ impl CompressionType {
} }
} }
impl ToString for CompressionType { impl Display for CompressionType {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Gzip => "gzip".to_string(), Self::Gzip => write!(f, "gzip"),
Self::Brotli => "br".to_string(), Self::Brotli => write!(f, "br"),
Self::Passthrough => String::new(), Self::Passthrough => Ok(()),
} }
} }
} }

View file

@ -19,18 +19,20 @@ struct SettingsTemplate {
// CONSTANTS // CONSTANTS
const PREFS: [&str; 15] = [ const PREFS: [&str; 17] = [
"theme", "theme",
"front_page", "front_page",
"layout", "layout",
"wide", "wide",
"comment_sort", "comment_sort",
"post_sort", "post_sort",
"blur_spoiler",
"show_nsfw", "show_nsfw",
"blur_nsfw", "blur_nsfw",
"use_hls", "use_hls",
"hide_hls_notification", "hide_hls_notification",
"autoplay_videos", "autoplay_videos",
"hide_sidebar_and_summary",
"fixed_navbar", "fixed_navbar",
"hide_awards", "hide_awards",
"hide_score", "hide_score",

View file

@ -8,6 +8,8 @@ use cookie::Cookie;
use hyper::header::CONTENT_TYPE; use hyper::header::CONTENT_TYPE;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy;
use regex::Regex;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
// STRUCTS // STRUCTS
@ -51,10 +53,13 @@ struct WallTemplate {
url: String, url: String,
} }
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
// SERVICES // SERVICES
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path // Build Reddit API path
let root = req.uri().path() == "/"; let root = req.uri().path() == "/";
let query = req.uri().query().unwrap_or_default().to_string();
let subscribed = setting(&req, "subscriptions"); let subscribed = setting(&req, "subscriptions");
let front_page = setting(&req, "front_page"); let front_page = setting(&req, "front_page");
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
@ -108,10 +113,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let mut params = String::from("&raw_json=1"); let mut params = String::from("&raw_json=1");
if sub_name == "popular" { if sub_name == "popular" {
params.push_str("&geo_filter=GLOBAL"); let geo_filter = match GEO_FILTER_MATCH.captures(&query) {
Some(geo_filter) => geo_filter["region"].to_string(),
None => "GLOBAL".to_owned(),
};
params.push_str(&format!("&geo_filter={geo_filter}"));
} }
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default()); let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B"); let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
let filters = get_filters(&req); let filters = get_filters(&req);
@ -137,6 +146,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty(); let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
if sort == "new" {
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
}
Ok(template(&SubredditTemplate { Ok(template(&SubredditTemplate {
sub, sub,
posts, posts,

View file

@ -1,3 +1,4 @@
#![allow(dead_code)]
use crate::config::get_setting; use crate::config::get_setting;
// //
// CRATES // CRATES
@ -156,6 +157,7 @@ impl PollOption {
// Post flags with nsfw and stickied // Post flags with nsfw and stickied
pub struct Flags { pub struct Flags {
pub spoiler: bool,
pub nsfw: bool, pub nsfw: bool,
pub stickied: bool, pub stickied: bool,
} }
@ -167,6 +169,7 @@ pub struct Media {
pub width: i64, pub width: i64,
pub height: i64, pub height: i64,
pub poster: String, pub poster: String,
pub download_name: String,
} }
impl Media { impl Media {
@ -233,6 +236,15 @@ impl Media {
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default())); let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
format!("redlib_{permalink_base}_{media_url_base}")
} else {
String::new()
};
( (
post_type.to_string(), post_type.to_string(),
Self { Self {
@ -243,6 +255,7 @@ impl Media {
width: source["width"].as_i64().unwrap_or_default(), width: source["width"].as_i64().unwrap_or_default(),
height: source["height"].as_i64().unwrap_or_default(), height: source["height"].as_i64().unwrap_or_default(),
poster: format_url(source["url"].as_str().unwrap_or_default()), poster: format_url(source["url"].as_str().unwrap_or_default()),
download_name,
}, },
gallery, gallery,
) )
@ -296,6 +309,7 @@ pub struct Post {
pub body: String, pub body: String,
pub author: Author, pub author: Author,
pub permalink: String, pub permalink: String,
pub link_title: String,
pub poll: Option<Poll>, pub poll: Option<Poll>,
pub score: (String, String), pub score: (String, String),
pub upvote_ratio: i64, pub upvote_ratio: i64,
@ -307,6 +321,7 @@ pub struct Post {
pub domain: String, pub domain: String,
pub rel_time: String, pub rel_time: String,
pub created: String, pub created: String,
pub created_ts: u64,
pub num_duplicates: u64, pub num_duplicates: u64,
pub comments: (String, String), pub comments: (String, String),
pub gallery: Vec<GalleryMedia>, pub gallery: Vec<GalleryMedia>,
@ -338,6 +353,7 @@ impl Post {
let data = &post["data"]; let data = &post["data"];
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default()); let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
let score = data["score"].as_i64().unwrap_or_default(); let score = data["score"].as_i64().unwrap_or_default();
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0; let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = val(post, "title"); let title = val(post, "title");
@ -384,6 +400,7 @@ impl Post {
width: data["thumbnail_width"].as_i64().unwrap_or_default(), width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(), height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(), poster: String::new(),
download_name: String::new(),
}, },
media, media,
domain: val(post, "domain"), domain: val(post, "domain"),
@ -402,13 +419,16 @@ impl Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: data["spoiler"].as_bool().unwrap_or_default(),
nsfw: data["over_18"].as_bool().unwrap_or_default(), nsfw: data["over_18"].as_bool().unwrap_or_default(),
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(), stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
}, },
permalink: val(post, "permalink"), permalink: val(post, "permalink"),
link_title: val(post, "link_title"),
poll: Poll::parse(&data["poll_data"]), poll: Poll::parse(&data["poll_data"]),
rel_time, rel_time,
created, created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0), num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()), comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
gallery, gallery,
@ -417,7 +437,6 @@ impl Post {
ws_url: val(post, "websocket_url"), ws_url: val(post, "websocket_url"),
}); });
} }
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string())) Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
} }
} }
@ -574,9 +593,11 @@ pub struct Preferences {
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
pub blur_spoiler: String,
pub show_nsfw: String, pub show_nsfw: String,
pub blur_nsfw: String, pub blur_nsfw: String,
pub hide_hls_notification: String, pub hide_hls_notification: String,
pub hide_sidebar_and_summary: String,
pub use_hls: String, pub use_hls: String,
pub autoplay_videos: String, pub autoplay_videos: String,
pub fixed_navbar: String, pub fixed_navbar: String,
@ -610,7 +631,9 @@ impl Preferences {
front_page: setting(req, "front_page"), front_page: setting(req, "front_page"),
layout: setting(req, "layout"), layout: setting(req, "layout"),
wide: setting(req, "wide"), wide: setting(req, "wide"),
blur_spoiler: setting(req, "blur_spoiler"),
show_nsfw: setting(req, "show_nsfw"), show_nsfw: setting(req, "show_nsfw"),
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
blur_nsfw: setting(req, "blur_nsfw"), blur_nsfw: setting(req, "blur_nsfw"),
use_hls: setting(req, "use_hls"), use_hls: setting(req, "use_hls"),
hide_hls_notification: setting(req, "hide_hls_notification"), hide_hls_notification: setting(req, "hide_hls_notification"),
@ -666,6 +689,8 @@ pub async fn parse_post(post: &Value) -> Post {
// Determine the type of media along with the media URL // Determine the type of media along with the media URL
let (post_type, media, gallery) = Media::parse(&post["data"]).await; let (post_type, media, gallery) = Media::parse(&post["data"]).await;
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]); let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
let permalink = val(post, "permalink"); let permalink = val(post, "permalink");
@ -702,6 +727,7 @@ pub async fn parse_post(post: &Value) -> Post {
distinguished: val(post, "distinguished"), distinguished: val(post, "distinguished"),
}, },
permalink, permalink,
link_title: val(post, "link_title"),
poll, poll,
score: format_num(score), score: format_num(score),
upvote_ratio: ratio as i64, upvote_ratio: ratio as i64,
@ -713,6 +739,7 @@ pub async fn parse_post(post: &Value) -> Post {
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(), width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(), height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(), poster: String::new(),
download_name: String::new(),
}, },
flair: Flair { flair: Flair {
flair_parts: FlairPart::parse( flair_parts: FlairPart::parse(
@ -729,12 +756,14 @@ pub async fn parse_post(post: &Value) -> Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: post["data"]["spoiler"].as_bool().unwrap_or_default(),
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false), stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
}, },
domain: val(post, "domain"), domain: val(post, "domain"),
rel_time, rel_time,
created, created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0), num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()), comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
gallery, gallery,
@ -875,9 +904,9 @@ pub fn format_url(url: &str) -> String {
// These are links we want to replace in-body // These are links we want to replace in-body
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap()); static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap());
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview)\.redd\.it(.*)[^?]").unwrap()); static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap());
static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap()); static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap());
static REDLIB_PREVIEW_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"/preview/(pre|external-pre)/(.*?)>"#).unwrap()); static REDLIB_PREVIEW_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"/(img|preview/)(pre|external-pre)?/(.*?)>"#).unwrap());
static REDLIB_PREVIEW_TEXT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r">(.*?)</a>").unwrap()); static REDLIB_PREVIEW_TEXT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r">(.*?)</a>").unwrap());
// Rewrite Reddit links to Redlib in body of text // Rewrite Reddit links to Redlib in body of text
@ -901,14 +930,47 @@ pub fn rewrite_urls(input_text: &str) -> String {
let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()); let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default());
let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string(); let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
let image_text = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string(); let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
let image_to_replace = format!("<a href=\"{image_url}{image_text}").replace(">>", ">"); /* As long as image_caption isn't empty remove first and last four characters of image_text to leave us with just the text in the caption without any HTML.
let image_replacement = format!("<a href=\"{image_url}<img src=\"{image_url}</a>"); This makes it possible to enclose it in a <figcaption> later on without having stray HTML breaking it */
if !image_caption.is_empty() {
image_caption = image_caption[1..image_caption.len() - 4].to_string();
}
// image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them
let image_to_replace = format!("<a href=\"{image_url}{image_caption}</a>");
// _image_replacement needs to be in scope for the replacement at the bottom of the loop
let mut _image_replacement = String::new();
/* We don't want to show a caption that's just the image's link, so we check if we find a Reddit preview link within the image's caption.
If we don't find one we must have actual text, so we include a <figcaption> block that contains it.
Otherwise we don't include the <figcaption> block as we don't need it. */
if REDDIT_PREVIEW_REGEX.find(&image_caption).is_none() {
// Without this " would show as \" instead. "\&quot;" is how the quotes are formatted within image_text beforehand
image_caption = image_caption.replace("\\&quot;", "\"");
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a><figcaption>{image_caption}</figcaption></figure>");
} else {
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a></figure>");
}
/* In order to know if we're dealing with a normal or external preview we need to take a look at the first capture group of REDDIT_PREVIEW_REGEX
if it's preview we're dealing with something that needs /preview/pre, external-preview is /preview/external-pre, and i is /img */
let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()).to_string();
let mut _preview_type = String::new();
if reddit_preview_regex_capture == "preview" {
_preview_type = "/preview/pre".to_string();
} else if reddit_preview_regex_capture == "external-preview" {
_preview_type = "/preview/external-pre".to_string();
} else {
_preview_type = "/img".to_string();
}
text1 = REDDIT_PREVIEW_REGEX text1 = REDDIT_PREVIEW_REGEX
.replace(&text1, formatted_url) .replace(&text1, format!("{_preview_type}$2"))
.replace(&image_to_replace, &image_replacement) .replace(&image_to_replace, &_image_replacement)
.to_string() .to_string()
} }
} }
@ -993,7 +1055,7 @@ pub fn redirect(path: &str) -> Response<Body> {
/// Renders a generic error landing page. /// Renders a generic error landing page.
pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> { pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
error!("Error page rendered: {msg}"); error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
let url = req.uri().to_string(); let url = req.uri().to_string();
let body = ErrorTemplate { let body = ErrorTemplate {
msg: msg.to_string(), msg: msg.to_string(),
@ -1061,6 +1123,20 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default()) Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
} }
// Returns the last (non-empty) segment of a path string
pub fn url_path_basename(path: &str) -> String {
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
if url_result.is_err() {
path.to_string()
} else {
let mut url = url_result.unwrap();
url.path_segments_mut().unwrap().pop_if_empty();
url.path_segments().unwrap().last().unwrap().to_string()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{format_num, format_url, rewrite_urls}; use super::{format_num, format_url, rewrite_urls};
@ -1164,11 +1240,24 @@ async fn test_fetching_ws() {
#[test] #[test]
fn test_rewriting_image_links() { fn test_rewriting_image_links() {
let input = r#"<p><a href="https://preview.redd.it/zq21ggkj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=539d8050628ec1190cac26468fe99cc66b6071ab">https://preview.redd.it/zq21ggkj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=539d8050628ec1190cac26468fe99cc66b6071ab</a></p> let input =
<p><a href="https://preview.redd.it/vty9ocij2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877">https://preview.redd.it/vty9ocij2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877</a></p> r#"<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>"#;
<p><a href="https://preview.redd.it/bdfdxkjj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=d0fa420ece27605e882e89cb4711d75d774322ac">https://preview.redd.it/bdfdxkjj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=d0fa420ece27605e882e89cb4711d75d774322ac</a></p> let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>
<p><a href="https://preview.redd.it/rbu2ca2b2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=afb538cf784d2e339de9a91aba5dc9c92e47988f">caption 2</a></p>"#;
let output = r#"<p><a href="/preview/pre/zq21ggkj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=539d8050628ec1190cac26468fe99cc66b6071ab"><img src="/preview/pre/zq21ggkj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=539d8050628ec1190cac26468fe99cc66b6071ab"></a></p> <p><a href="/preview/pre/vty9ocij2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877"><img src="/preview/pre/vty9ocij2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877"></a></p> <p><a href="/preview/pre/bdfdxkjj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=d0fa420ece27605e882e89cb4711d75d774322ac"><img src="/preview/pre/bdfdxkjj2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=d0fa420ece27605e882e89cb4711d75d774322ac"></a></p> <p><a href="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img src="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a></p> <p><a href="/preview/pre/rbu2ca2b2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=afb538cf784d2e339de9a91aba5dc9c92e47988f"><img src="/preview/pre/rbu2ca2b2xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=afb538cf784d2e339de9a91aba5dc9c92e47988f"></a></p>"#;
assert_eq!(rewrite_urls(input), output); assert_eq!(rewrite_urls(input), output);
} }
#[test]
fn test_url_path_basename() {
// without trailing slash
assert_eq!(url_path_basename("/first/last"), "last");
// with trailing slash
assert_eq!(url_path_basename("/first/last/"), "last");
// with query parameters
assert_eq!(url_path_basename("/first/last/?some=query"), "last");
// file path
assert_eq!(url_path_basename("/cdn/image.jpg"), "image.jpg");
// when a full url is passed instead of just a path
assert_eq!(url_path_basename("https://doma.in/first/last"), "last");
// empty path
assert_eq!(url_path_basename("/"), "");
}

View file

@ -34,6 +34,7 @@
font-family: 'Inter'; font-family: 'Inter';
src: url('/Inter.var.woff2') format('woff2-variations'); src: url('/Inter.var.woff2') format('woff2-variations');
font-style: normal; font-style: normal;
font-weight: 100 900;
} }
/* Automatic theme selection */ /* Automatic theme selection */
@ -51,6 +52,7 @@
--visited: #aaa; --visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5); --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
--popup: #b80a27; --popup: #b80a27;
--spoiler: #ddd;
/* Hint color theme to browser for scrollbar */ /* Hint color theme to browser for scrollbar */
color-scheme: dark; color-scheme: dark;
@ -70,6 +72,7 @@
--highlighted: white; --highlighted: white;
--visited: #555; --visited: #555;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--spoiler: #0f0f0f;
/* Hint color theme to browser for scrollbar */ /* Hint color theme to browser for scrollbar */
color-scheme: light; color-scheme: light;
@ -187,6 +190,11 @@ nav #redlib {
vertical-align: -2px; vertical-align: -2px;
} }
figcaption {
margin-top: 5px;
text-align: center;
}
#settings_link { #settings_link {
opacity: 0.8; opacity: 0.8;
margin-left: 10px; margin-left: 10px;
@ -644,7 +652,6 @@ button.submit:hover > svg { stroke: var(--accent); }
} }
#sort_options + #timeframe:not(#search_sort > #timeframe) { #sort_options + #timeframe:not(#search_sort > #timeframe) {
margin-left: 10px;
border-radius: 5px 0px 0px 5px; border-radius: 5px 0px 0px 5px;
} }
@ -654,24 +661,17 @@ button.submit:hover > svg { stroke: var(--accent); }
} }
#search_sort { #search_sort {
background: var(--highlighted);
border-radius: 5px; border-radius: 5px;
overflow: auto; overflow: auto;
} }
#search_sort > #search { #search_sort > *, .search_widget_divider_box > *, .search_widget_divider_box #sort_options {
border: 0; background: var(--highlighted);
background: transparent; font-size: 15px;
} }
#search_sort > *, #searchbox > * { font-size: 15px; } #search_sort > #search {
border: 0;
#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; background: transparent;
} }
@ -690,6 +690,66 @@ button.submit:hover > svg { stroke: var(--accent); }
margin-bottom: 20px; margin-bottom: 20px;
} }
#search_sort > .search_widget_divider_box {
width: 100%;
}
.search_widget_divider_box > * {
border-right: 2px var(--outside) solid;
margin: 0;
}
.search_widget_divider_box {
display: flex;
align-items: center;
max-width: 100%;
border: 0;
}
.search_widget_divider_box > #sort_options {
border-radius: 0;
box-shadow: none;
}
/* When screen size is smaller than 480px we switch to a design better suited for mobile devices */
@media screen and (max-width: 480px) {
#search_sort {
align-items: unset;
}
.search_widget_divider_box > #search {
flex: 1;
min-width: unset;
border-right: 0;
border-bottom: 2px var(--outside) solid;
}
#search:focus {
outline: 0;
}
#search_sort > .search_widget_divider_box {
flex-wrap: wrap;
}
.search_widget_divider_box > * {
width: 100%;
}
#sort_options {
min-width: fit-content;
}
.search_widget_divider_box > select:last-child {
border-right: 0;
}
#sort_submit {
height: unset;
border-left: 2px var(--outside) solid;
}
}
#sort_options, #listing_options, main > * > footer > a { #sort_options, #listing_options, main > * > footer > a {
border-radius: 5px; border-radius: 5px;
align-items: center; align-items: center;
@ -923,6 +983,15 @@ a.search_subreddit:hover {
font-weight: bold; font-weight: bold;
} }
.spoiler {
color: var(--spoiler);
margin-left: 5px;
border: 1px solid var(--spoiler);
padding: 3px;
font-size: 12px;
border-radius: 5px;
}
.post_media_content, .post .__NoScript_PlaceHolder__, .gallery { .post_media_content, .post .__NoScript_PlaceHolder__, .gallery {
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
grid-area: post_media; grid-area: post_media;
@ -950,11 +1019,25 @@ a.search_subreddit:hover {
margin: auto; margin: auto;
} }
.post_nsfw_blur { .post_blurred img,
.post_blurred svg,
.post_blurred video {
filter: blur(1.5rem); filter: blur(1.5rem);
} }
.post_nsfw_blur:hover { .post_blurred .post_body {
filter: blur(0.25rem);
}
.post_blurred .post_thumbnail svg {
filter: blur(0.3rem);
}
.post_blurred img:hover,
.post_blurred svg:hover,
.post_blurred video:hover,
.post_blurred .post_body:hover,
.post_blurred .post_thumbnail:hover svg {
filter: none; filter: none;
} }
@ -979,10 +1062,6 @@ a.search_subreddit:hover {
vertical-align: bottom; vertical-align: bottom;
} }
.gallery figcaption {
margin-top: 5px;
}
.gallery .outbound_url { .gallery .outbound_url {
color: var(--accent); color: var(--accent);
text-overflow: ellipsis; text-overflow: ellipsis;
@ -1010,6 +1089,9 @@ a.search_subreddit:hover {
.post_body img { .post_body img {
max-width: 100%; max-width: 100%;
display: block;
margin-left: auto;
margin-right: auto;
} }
.post_poll { .post_poll {
@ -1094,12 +1176,12 @@ a.search_subreddit:hover {
margin-right: 15px; margin-right: 15px;
} }
#post_links > li.desktop_item { .desktop_item {
display: auto; display: auto;
} }
@media screen and (min-width: 480px) { @media screen and (min-width: 481px) {
#post_links > li.mobile_item { .mobile_item {
display: none; display: none;
} }
} }
@ -1131,10 +1213,6 @@ a.search_subreddit:hover {
z-index: 0; z-index: 0;
} }
.thumb_nsfw_blur {
filter: blur(0.3rem)
}
.post_thumbnail.no_thumbnail { .post_thumbnail.no_thumbnail {
background-color: var(--highlighted); background-color: var(--highlighted);
} }
@ -1187,6 +1265,10 @@ a.search_subreddit:hover {
} }
} }
.comment figure {
margin: 0;
}
.comment_left, .comment_right { .comment_left, .comment_right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1213,10 +1295,6 @@ a.search_subreddit:hover {
font-weight: bold; font-weight: bold;
} }
.comment_subreddit {
font-weight: bold;
}
.comment_score { .comment_score {
color: var(--accent); color: var(--accent);
background: var(--foreground); background: var(--foreground);
@ -1235,10 +1313,28 @@ a.search_subreddit:hover {
min-width: 0; min-width: 0;
} }
.comment_data > * { .comment:has([id]) .comment_data > * {
margin-right: 5px; margin-right: 5px;
} }
.comment:not([id]) .comment_data {
display: inline-flex;
max-width: 100%;
}
.comment:not([id]) .comment_data > * {
flex: 0 0 auto;
}
.comment:not([id]) .comment_data > .comment_link {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
overflow: hidden;
flex: 0 1 auto;
}
.comment_image { .comment_image {
max-width: 500px; max-width: 500px;
align-self: center; align-self: center;
@ -1308,6 +1404,15 @@ summary.comment_data {
cursor: pointer; cursor: pointer;
} }
.user_comment_data_divider {
display: flex;
align-items: center;
}
.user_comment_data_divider .dot {
display: none;
}
.moderator, .admin { opacity: 1; } .moderator, .admin { opacity: 1; }
.op, .moderator, .admin { font-weight: bold; } .op, .moderator, .admin { font-weight: bold; }
@ -1484,10 +1589,19 @@ input[type="submit"] {
width: 100%; width: 100%;
} }
.md > *:not(:first-child) { .md > p:not(:first-child):not(:last-child) {
margin-top: 20px; margin-top: 20px;
} }
.md > figure:first-of-type {
margin-top: 5px;
margin-bottom: 0px;
}
.md > figure:not(:first-of-type) {
margin-top: 10px;
}
.md h1 { font-size: 22px; } .md h1 { font-size: 22px; }
.md h2 { font-size: 20px; } .md h2 { font-size: 20px; }
.md h3 { font-size: 18px; } .md h3 { font-size: 18px; }
@ -1711,8 +1825,23 @@ td, th {
.comment_right { padding: 5px 0 10px 2px; } .comment_right { padding: 5px 0 10px 2px; }
.comment_author { margin-left: 12px; } .comment_author { margin-left: 12px; }
.comment_data { margin-left: 12px; } .comment_data { margin-left: 12px; }
.user-comment .comment_data {
flex-direction: column;
flex-wrap: wrap;
row-gap: 5px;
}
.comment_data::marker { font-size: 25px; } .comment_data::marker { font-size: 25px; }
.created { width: 100%; } .user-comment .comment_data > .comment_link { order: 2 }
.user_comment_data_divider { order: 1; }
.user_comment_data_divider .dot {
display: unset;
margin-left: 5px;
}
.created-in { display: none; }
.comment_score { .comment_score {
min-width: 32px; min-width: 32px;
@ -1723,10 +1852,11 @@ td, th {
} }
#post_links > li { margin-right: 10px } #post_links > li { margin-right: 10px }
#post_links > li.desktop_item { display: none }
#post_links > li.mobile_item { display: auto }
.post_footer > p > span#upvoted { display: none } .post_footer > p > span#upvoted { display: none }
.desktop_item { display: none }
.mobile_item { display: auto }
.popup { .popup {
width: auto; width: auto;
} }

View file

@ -0,0 +1,14 @@
/* icebergDark theme setting */
.icebergDark {
--accent: #85a0c7;
--green: #b5bf82;
--text: #c6c8d1;
--foreground: #454d73;
--background: #161821;
--outside: #1f2233;
--post: #1f2233;
--panel-border: 1px solid #454d73;
--highlighted: #0f1117;
--visited: #0f1117;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

View file

@ -2,10 +2,14 @@
{% block title %}Error: {{ msg }}{% endblock %} {% block title %}Error: {{ msg }}{% endblock %}
{% block sortstyle %}{% endblock %} {% block sortstyle %}{% endblock %}
{% block content %} {% block content %}
<div id="error"> <div id="error">
<h1>{{ msg }}</h1> <h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3> <h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br /> <br />
<h3>Head back <a href="/">home</a>?</h3> <h3>Expected something to work? <a
</div> href="https://github.com/redlib-org/redlib/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=%F0%9F%90%9B+Bug+Report%3A+{{ msg }}">Report
an issue</a></h3>
<br />
<h3>Head back <a href="/">home</a>?</h3>
</div>
{% endblock %} {% endblock %}

View file

@ -10,25 +10,34 @@
{% block content %} {% block content %}
<div id="column_one"> <div id="column_one">
<form id="search_sort"> <form id="search_sort">
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib"> <div class="search_widget_divider_box">
{% if sub != "" %} <input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
<div id="inside"> <div class="search_widget_divider_box">
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}> {% if sub != "" %}
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label> <div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label>
</div>
{% endif %}
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %}
<select id="sort_options" name="sort" title="Sort results by">
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
</select>
{% if params.sort != "new" %}
<select id="timeframe" name="t" title="Timeframe">
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>
{% endif %}
</div>
</div> </div>
{% endif %}
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %} <button id="sort_submit" class="submit">
<select id="sort_options" name="sort" title="Sort results by"> <svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %} <path d="M20 50 H100" />
</select>{% if params.sort != "new" %}<select id="timeframe" name="t" title="Timeframe"> <path d="M75 15 L100 50 L75 85" />
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %} &rarr;
</select>{% endif %}<button id="sort_submit" class="submit"> </svg>
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round"> </button>
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
</svg>
</button>
</form> </form>
{% if !is_filtered %} {% if !is_filtered %}

View file

@ -54,6 +54,11 @@
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select> </select>
</div> </div>
<div class="prefs-group">
<label for="blur_spoiler">Blur spoiler previews:</label>
<input type="hidden" value="off" name="blur_spoiler">
<input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}>
</div>
{% if !crate::utils::sfw_only() %} {% if !crate::utils::sfw_only() %}
<div class="prefs-group"> <div class="prefs-group">
<label for="show_nsfw">Show NSFW posts:</label> <label for="show_nsfw">Show NSFW posts:</label>
@ -71,11 +76,16 @@
<input type="hidden" value="off" name="autoplay_videos"> <input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}> <input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group"> <div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label> <label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar"> <input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}> <input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group"> <div class="prefs-group">
<label for="use_hls">Use HLS for videos</label> <label for="use_hls">Use HLS for videos</label>
<details id="feeds"> <details id="feeds">

View file

@ -82,7 +82,7 @@
</footer> </footer>
</div> </div>
{% endif %} {% endif %}
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %} {% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) && prefs.hide_sidebar_and_summary != "on" %}
<aside> <aside>
{% if is_filtered %} {% if is_filtered %}
<center>(Content from r/{{ sub.name }} has been filtered)</center> <center>(Content from r/{{ sub.name }} has been filtered)</center>

View file

@ -50,7 +50,7 @@
{% else if !post.title.is_empty() %} {% else if !post.title.is_empty() %}
{% call utils::post_in_list(post) %} {% call utils::post_in_list(post) %}
{% else %} {% else %}
<div class="comment"> <div class="comment user-comment">
<div class="comment_left"> <div class="comment_left">
<p class="comment_score" title="{{ post.score.1 }}"> <p class="comment_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %} {% if prefs.hide_score != "on" %}
@ -63,8 +63,13 @@
</div> </div>
<details class="comment_right" open> <details class="comment_right" open>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_link" href="{{ post.permalink }}">Comment on r/{{ post.community }}</a> <a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a>
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span> <div class="user_comment_data_divider">
<span class="created-in">&nbsp;in&nbsp;</span>
<a class="comment_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span>
<span class="created" title="{{ post.created }}">&nbsp;{{ post.rel_time }}</span>
</div>
</summary> </summary>
<p class="comment_body">{{ post.body|safe }}</p> <p class="comment_body">{{ post.body|safe }}</p>
</details> </details>

View file

@ -62,8 +62,9 @@
{%- endmacro %} {%- endmacro %}
{% macro post(post) -%} {% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT --> <!-- POST CONTENT -->
<div class="post highlighted"> <div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
<p class="post_header"> <p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
@ -93,6 +94,7 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a> style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} {% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h1> </h1>
<!-- POST MEDIA --> <!-- POST MEDIA -->
@ -101,7 +103,7 @@
<div class="post_media_content"> <div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image" > <a href="{{ post.media.url }}" class="post_media_image" >
{% if post.media.height == 0 || post.media.width == 0 %} {% if post.media.height == 0 || post.media.width == 0 %}
<!-- i.redd.it images speical case --> <!-- i.redd.it images special case -->
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
@ -161,13 +163,28 @@
<span class="label"> Upvotes</span></div> <span class="label"> Upvotes</span></div>
<div class="post_footer"> <div class="post_footer">
<ul id="post_links"> <ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li> <li>
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li> <a href="{{ post.permalink }}">
<span class="desktop_item">perma</span>link
</a>
</li>
{% if post.num_duplicates > 0 %} {% if post.num_duplicates > 0 %}
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li> <li>
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li> <a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
dup<span class="desktop_item">licat</span>es
</a>
</li>
{% endif %} {% endif %}
{% call external_reddit_link(post.permalink) %} {% call external_reddit_link(post.permalink) %}
{% if post.media.download_name != "" %}
<li>
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
<span class="mobile_item">dl</span>
<span class="desktop_item">download</span>
</a>
</li>
{% endif %}
</ul> </ul>
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p> <p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
</div> </div>
@ -175,8 +192,7 @@
{%- endmacro %} {%- endmacro %}
{% macro external_reddit_link(permalink) %} {% macro external_reddit_link(permalink) %}
{% for dev_type in ["desktop", "mobile"] %} <li>
<li class="{{ dev_type }}_item">
<a <a
{% if prefs.disable_visit_reddit_confirmation != "on" %} {% if prefs.disable_visit_reddit_confirmation != "on" %}
href="#popup" href="#popup"
@ -190,11 +206,11 @@
{% call visit_reddit_confirmation(permalink) %} {% call visit_reddit_confirmation(permalink) %}
{% endif %} {% endif %}
</li> </li>
{% endfor %}
{% endmacro %} {% endmacro %}
{% macro post_in_list(post) -%} {% macro post_in_list(post) -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}"> {% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
<p class="post_header"> <p class="post_header">
{% let community -%} {% let community -%}
{% if post.community.starts_with("u_") -%} {% if post.community.starts_with("u_") -%}
@ -222,7 +238,7 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a> dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} <a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h2> </h2>
<!-- POST MEDIA/THUMBNAIL --> <!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %} {% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
@ -233,7 +249,6 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -245,26 +260,22 @@
{% endif %} {% endif %}
</a> </a>
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && (post.post_type == "gif" || post.post_type == "video") %}
<div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none"> <video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %} {% endif %}
{% else if post.post_type != "self" %} {% else if post.post_type != "self" %}
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow"> <a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
{% if post.thumbnail.url.is_empty() %} {% if post.thumbnail.url.is_empty() %}
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
<title>Thumbnail</title> <title>Thumbnail</title>
@ -272,7 +283,7 @@
</svg> </svg>
{% else %} {% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;"> <div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg"> <svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/> <image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc> <desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/> <img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>