mirror of
https://github.com/redlib-org/redlib.git
synced 2025-06-10 16:39:41 +00:00
Merge branch 'main' into rss
This commit is contained in:
commit
71d9d0ded4
35 changed files with 1098 additions and 528 deletions
|
@ -24,6 +24,8 @@ REDLIB_DEFAULT_WIDE=off
|
|||
REDLIB_DEFAULT_POST_SORT=hot
|
||||
# Set the default comment sort method (options: confidence, top, new, controversial, old)
|
||||
REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||
# Enable blurring Spoiler content by default
|
||||
REDLIB_DEFAULT_BLUR_SPOILER=off
|
||||
# Enable showing NSFW content by default
|
||||
REDLIB_DEFAULT_SHOW_NSFW=off
|
||||
# Enable blurring NSFW content by default
|
||||
|
@ -36,8 +38,12 @@ REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
|||
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
|
||||
REDLIB_DEFAULT_SUBSCRIPTIONS=
|
||||
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
|
||||
REDLIB_DEFAULT_FILTERS=
|
||||
# Hide awards by default
|
||||
REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||
# Hide sidebar and summary
|
||||
REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY=off
|
||||
# Disable the confirmation before visiting Reddit
|
||||
REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
||||
# Hide score by default
|
||||
|
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake
|
3
.github/workflows/build-artifacts.yaml
vendored
3
.github/workflows/build-artifacts.yaml
vendored
|
@ -7,6 +7,8 @@ on:
|
|||
- "compose.*"
|
||||
branches:
|
||||
- "main"
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
@ -60,7 +62,6 @@ jobs:
|
|||
|
||||
- name: Upload release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.base_ref != 'main' && github.event_name == 'release'
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.VERSION }}
|
||||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
||||
|
|
16
.github/workflows/main-rust.yml
vendored
16
.github/workflows/main-rust.yml
vendored
|
@ -30,9 +30,15 @@ jobs:
|
|||
with:
|
||||
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
|
||||
- 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
|
||||
id: version
|
||||
|
@ -45,17 +51,17 @@ jobs:
|
|||
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- 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
|
||||
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
|
||||
name: Upload a Build Artifact
|
||||
with:
|
||||
name: redlib
|
||||
path: |
|
||||
target/x86_64-unknown-linux-gnu/release/redlib
|
||||
target/x86_64-unknown-linux-musl/release/redlib
|
||||
redlib.sha512
|
||||
redlib.sha256
|
||||
|
||||
|
@ -68,7 +74,7 @@ jobs:
|
|||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
||||
draft: true
|
||||
files: |
|
||||
target/x86_64-unknown-linux-gnu/release/redlib
|
||||
target/x86_64-unknown-linux-musl/release/redlib
|
||||
redlib.sha512
|
||||
redlib.sha256
|
||||
body: |
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,4 +1,10 @@
|
|||
/target
|
||||
.env
|
||||
redlib.toml
|
||||
|
||||
# Idea Files
|
||||
.idea/
|
||||
|
||||
# nix files
|
||||
.direnv/
|
||||
result
|
||||
|
|
2
.replit
2
.replit
|
@ -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"
|
||||
|
|
539
Cargo.lock
generated
539
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ name = "redlib"
|
|||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/redlib-org/redlib"
|
||||
version = "0.31.2"
|
||||
version = "0.35.1"
|
||||
authors = [
|
||||
"Matthew Esposito <matt+cargo@matthew.science>",
|
||||
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
||||
|
@ -12,7 +12,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
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 = [
|
||||
"std",
|
||||
"env",
|
||||
|
@ -31,18 +31,19 @@ time = { version = "0.3.31", features = ["local-offset"] }
|
|||
url = "2.5.0"
|
||||
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
||||
libflate = "2.0.0"
|
||||
brotli = { version = "3.4.0", features = ["std"] }
|
||||
brotli = { version = "6.0.0", features = ["std"] }
|
||||
toml = "0.8.8"
|
||||
once_cell = "1.19.0"
|
||||
serde_yaml = "0.9.29"
|
||||
build_html = "2.4.0"
|
||||
uuid = { version = "1.6.1", features = ["v4"] }
|
||||
base64 = "0.21.5"
|
||||
base64 = "0.22.1"
|
||||
fastrand = "2.0.1"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5.0"
|
||||
dotenvy = "0.15.7"
|
||||
rss = "2.0.7"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
lipsum = "0.9.0"
|
||||
|
|
|
@ -14,7 +14,7 @@ USER redlib
|
|||
EXPOSE 8080
|
||||
|
||||
# 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"]
|
||||
|
||||
|
|
20
README.md
20
README.md
|
@ -203,12 +203,6 @@ docker logs -f redlib
|
|||
|
||||
### 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:
|
||||
|
||||
```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.
|
||||
|
||||
| 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. |
|
||||
| `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. |
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
|
@ -393,12 +388,13 @@ Assign a default value for each user-modifiable setting by passing environment v
|
|||
|
||||
| Name | Possible values | Default value |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||
| `WIDE` | `["on", "off"]` | `off` |
|
||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
|
||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||
|
|
6
app.json
6
app.json
|
@ -29,6 +29,9 @@
|
|||
"REDLIB_DEFAULT_POST_SORT": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_BLUR_SPOILER": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_SHOW_NSFW": {
|
||||
"required": false
|
||||
},
|
||||
|
@ -59,6 +62,9 @@
|
|||
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_FILTERS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
|
||||
"required": false
|
||||
},
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
services:
|
||||
redlib:
|
||||
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
|
||||
container_name: "redlib"
|
||||
ports:
|
||||
|
|
|
@ -6,6 +6,7 @@ PORT=12345
|
|||
#REDLIB_DEFAULT_WIDE=off
|
||||
#REDLIB_DEFAULT_POST_SORT=hot
|
||||
#REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||
#REDLIB_DEFAULT_BLUR_SPOILER=off
|
||||
#REDLIB_DEFAULT_SHOW_NSFW=off
|
||||
#REDLIB_DEFAULT_BLUR_NSFW=off
|
||||
#REDLIB_DEFAULT_USE_HLS=off
|
||||
|
|
106
flake.lock
generated
Normal file
106
flake.lock
generated
Normal 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
71
flake.nix
Normal 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
31
scripts/load_test.py
Normal 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
|
|
@ -1,17 +1,20 @@
|
|||
use arc_swap::ArcSwap;
|
||||
use cached::proc_macro::cached;
|
||||
use futures_lite::future::block_on;
|
||||
use futures_lite::{future::Boxed, FutureExt};
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use libflate::gzip;
|
||||
use log::error;
|
||||
use log::{error, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::{percent_encode, CONTROLS};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16};
|
||||
use std::{io, result::Result};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::dbg_msg;
|
||||
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
|
||||
|
@ -19,6 +22,7 @@ use crate::server::RequestExt;
|
|||
use crate::utils::format_url;
|
||||
|
||||
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(|| {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
|
@ -30,12 +34,16 @@ pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
|||
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());
|
||||
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
|
||||
/// 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 (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.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(),
|
||||
)
|
||||
};
|
||||
|
||||
// Build request to Reddit. When making a GET, request gzip compression.
|
||||
// (Reddit doesn't do brotli yet.)
|
||||
let builder = Request::builder()
|
||||
|
@ -214,12 +223,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||
if !redirect {
|
||||
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(
|
||||
method,
|
||||
response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
location_header
|
||||
.map(|val| {
|
||||
// We need to make adjustments to the URI
|
||||
// we get back from Reddit. Namely, we
|
||||
|
@ -232,7 +242,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||
// required.
|
||||
//
|
||||
// 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 { "?" })
|
||||
})
|
||||
.unwrap_or_default()
|
||||
|
@ -291,7 +305,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
dbg_msg!("{} {}: {}", method, path, e);
|
||||
dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
|
||||
|
||||
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)]
|
||||
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
// 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);
|
||||
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...
|
||||
match reddit_get(path.clone(), quarantine).await {
|
||||
Ok(response) => {
|
||||
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
|
||||
match hyper::body::aggregate(response).await {
|
||||
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
|
||||
match serde_json::from_reader(body.reader()) {
|
||||
Ok(value) => {
|
||||
|
@ -331,7 +382,7 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
|||
let () = force_refresh_token().await;
|
||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
||||
}
|
||||
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"]))
|
||||
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
|
||||
} else {
|
||||
Ok(json)
|
||||
}
|
||||
|
@ -341,21 +392,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
|||
if status.is_server_error() {
|
||||
Err("Reddit is having issues, check if there's an outage".to_string())
|
||||
} 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")]
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,10 @@ pub struct Config {
|
|||
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
|
||||
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(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
|
||||
pub(crate) default_show_nsfw: Option<String>,
|
||||
|
@ -68,6 +72,10 @@ pub struct Config {
|
|||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
|
||||
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(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
|
||||
pub(crate) default_hide_score: Option<String>,
|
||||
|
@ -76,6 +84,10 @@ pub struct Config {
|
|||
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
|
||||
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(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
||||
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
|
||||
|
@ -122,13 +134,16 @@ impl Config {
|
|||
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
|
||||
default_wide: parse("REDLIB_DEFAULT_WIDE"),
|
||||
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_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
|
||||
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_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
|
||||
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
|
||||
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
|
||||
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
|
||||
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
|
||||
banner: parse("REDLIB_BANNER"),
|
||||
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_COMMENT_SORT" => config.default_comment_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_BLUR_NSFW" => config.default_blur_nsfw.clone(),
|
||||
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
|
||||
"REDLIB_DEFAULT_WIDE" => config.default_wide.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_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_BANNER" => config.banner.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()));
|
||||
}
|
||||
|
||||
#[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]
|
||||
#[sealed_test]
|
||||
fn test_pushshift() {
|
||||
|
|
|
@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||
}
|
||||
|
||||
if have_after {
|
||||
before = "t3_".to_owned();
|
||||
"t3_".clone_into(&mut before);
|
||||
before.push_str(&duplicates[0].id);
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||
if have_before {
|
||||
// The next batch will need to start from one after the
|
||||
// last post in the current batch.
|
||||
after = "t3_".to_owned();
|
||||
"t3_".clone_into(&mut after);
|
||||
after.push_str(&duplicates[l - 1].id);
|
||||
|
||||
// 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 {
|
||||
Ok(response) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,11 +141,13 @@ impl InstanceInfo {
|
|||
["Wide", &convert(&self.config.default_wide)],
|
||||
["Comment sort", &convert(&self.config.default_comment_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)],
|
||||
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
|
||||
["Use HLS", &convert(&self.config.default_use_hls)],
|
||||
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
|
||||
["Subscriptions", &convert(&self.config.default_subscriptions)],
|
||||
["Filters", &convert(&self.config.default_filters)],
|
||||
])
|
||||
.with_header_row(["Default preferences"]),
|
||||
);
|
||||
|
@ -173,11 +175,13 @@ impl InstanceInfo {
|
|||
Default wide: {:?}\n
|
||||
Default comment sort: {:?}\n
|
||||
Default post sort: {:?}\n
|
||||
Default blur Spoiler: {:?}\n
|
||||
Default show NSFW: {:?}\n
|
||||
Default blur NSFW: {:?}\n
|
||||
Default use HLS: {:?}\n
|
||||
Default hide HLS notification: {:?}\n
|
||||
Default subscriptions: {:?}\n",
|
||||
Default subscriptions: {:?}\n
|
||||
Default filters: {:?}\n",
|
||||
self.package_name,
|
||||
self.crate_version,
|
||||
self.git_commit,
|
||||
|
@ -195,11 +199,13 @@ impl InstanceInfo {
|
|||
self.config.default_wide,
|
||||
self.config.default_comment_sort,
|
||||
self.config.default_post_sort,
|
||||
self.config.default_blur_spoiler,
|
||||
self.config.default_show_nsfw,
|
||||
self.config.default_blur_nsfw,
|
||||
self.config.default_use_hls,
|
||||
self.config.default_hide_hls_notification,
|
||||
self.config.default_subscriptions,
|
||||
self.config.default_filters,
|
||||
)
|
||||
}
|
||||
StringType::Html => self.to_table(),
|
||||
|
|
|
@ -135,7 +135,7 @@ async fn main() {
|
|||
.long("address")
|
||||
.value_name("ADDRESS")
|
||||
.help("Sets address to listen on")
|
||||
.default_value("0.0.0.0")
|
||||
.default_value("[::]")
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
|
|
37
src/oauth.rs
37
src/oauth.rs
|
@ -1,12 +1,12 @@
|
|||
use std::{collections::HashMap, time::Duration};
|
||||
use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
|
||||
|
||||
use crate::{
|
||||
client::{CLIENT, OAUTH_CLIENT},
|
||||
client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
|
||||
oauth_resources::ANDROID_APP_VERSION_LIST,
|
||||
};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hyper::{client, Body, Method, Request};
|
||||
use log::info;
|
||||
use log::{info, trace};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
|
@ -98,21 +98,13 @@ impl Oauth {
|
|||
|
||||
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() {
|
||||
// Monitor for refreshing token
|
||||
loop {
|
||||
// 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
|
||||
let duration = Duration::from_secs(expires_in - 120);
|
||||
|
@ -125,13 +117,22 @@ pub async fn token_daemon() {
|
|||
|
||||
// Refresh token - in its own scope
|
||||
{
|
||||
OAUTH_CLIENT.write().await.refresh().await;
|
||||
force_refresh_token().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
|
@ -179,21 +180,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
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")]
|
||||
async fn test_oauth_client_refresh() {
|
||||
OAUTH_CLIENT.write().await.refresh().await.unwrap();
|
||||
force_refresh_token().await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
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")]
|
||||
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]
|
||||
|
|
|
@ -4,6 +4,44 @@
|
|||
// Filled in with real app versions
|
||||
pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
|
||||
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.29.0/Build 1059855",
|
||||
"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.45.0/Build 1281371",
|
||||
"Version 2023.47.0/Build 1303604",
|
||||
"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 2022.42.0/Build 638508",
|
||||
"Version 2022.43.0/Build 648277",
|
||||
"Version 2022.44.0/Build 664348",
|
||||
"Version 2022.45.0/Build 677985",
|
||||
"Version 2023.01.0/Build 709875",
|
||||
"Version 2023.02.0/Build 717912",
|
||||
"Version 2023.03.0/Build 729220",
|
||||
"Version 2023.04.0/Build 744681",
|
||||
"Version 2023.05.0/Build 755453",
|
||||
"Version 2023.06.0/Build 775017",
|
||||
"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.20.0/Build 943980",
|
||||
"Version 2023.20.1/Build 946732",
|
||||
"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 2022.20.0/Build 487703",
|
||||
"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.25.1/Build 516394",
|
||||
"Version 2022.25.2/Build 519915",
|
||||
"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.41.0/Build 630468",
|
||||
"Version 2022.41.1/Build 634168",
|
||||
"Version 2022.42.0/Build 638508",
|
||||
"Version 2022.43.0/Build 648277",
|
||||
"Version 2022.44.0/Build 664348",
|
||||
"Version 2022.45.0/Build 677985",
|
||||
"Version 2023.01.0/Build 709875",
|
||||
"Version 2023.02.0/Build 717912",
|
||||
"Version 2023.03.0/Build 729220",
|
||||
"Version 2023.04.0/Build 744681",
|
||||
"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",
|
||||
"Version 2022.10.0/Build 429896",
|
||||
"Version 2022.1.0/Build 402829",
|
||||
"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.18.0/Build 473740",
|
||||
"Version 2022.19.1/Build 482464",
|
||||
"Version 2022.20.0/Build 487703",
|
||||
"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.4.0/Build 411368",
|
||||
"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.8.0/Build 423906",
|
||||
"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] = &[""];
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
|
||||
use cached::proc_macro::cached;
|
||||
use cookie::Cookie;
|
||||
|
@ -15,6 +17,7 @@ use libflate::gzip;
|
|||
use route_recognizer::{Params, Router};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::Display,
|
||||
io,
|
||||
pin::Pin,
|
||||
result::Result,
|
||||
|
@ -65,12 +68,12 @@ impl CompressionType {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToString for CompressionType {
|
||||
fn to_string(&self) -> String {
|
||||
impl Display for CompressionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Gzip => "gzip".to_string(),
|
||||
Self::Brotli => "br".to_string(),
|
||||
Self::Passthrough => String::new(),
|
||||
Self::Gzip => write!(f, "gzip"),
|
||||
Self::Brotli => write!(f, "br"),
|
||||
Self::Passthrough => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,18 +19,20 @@ struct SettingsTemplate {
|
|||
|
||||
// CONSTANTS
|
||||
|
||||
const PREFS: [&str; 15] = [
|
||||
const PREFS: [&str; 17] = [
|
||||
"theme",
|
||||
"front_page",
|
||||
"layout",
|
||||
"wide",
|
||||
"comment_sort",
|
||||
"post_sort",
|
||||
"blur_spoiler",
|
||||
"show_nsfw",
|
||||
"blur_nsfw",
|
||||
"use_hls",
|
||||
"hide_hls_notification",
|
||||
"autoplay_videos",
|
||||
"hide_sidebar_and_summary",
|
||||
"fixed_navbar",
|
||||
"hide_awards",
|
||||
"hide_score",
|
||||
|
|
|
@ -8,6 +8,8 @@ use cookie::Cookie;
|
|||
use hyper::header::CONTENT_TYPE;
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
|
@ -51,10 +53,13 @@ struct WallTemplate {
|
|||
url: String,
|
||||
}
|
||||
|
||||
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
|
||||
|
||||
// SERVICES
|
||||
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build Reddit API path
|
||||
let root = req.uri().path() == "/";
|
||||
let query = req.uri().query().unwrap_or_default().to_string();
|
||||
let subscribed = setting(&req, "subscriptions");
|
||||
let front_page = setting(&req, "front_page");
|
||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||
|
@ -108,10 +113,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||
|
||||
let mut params = String::from("&raw_json=1");
|
||||
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 redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
|
||||
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 no_posts = posts.is_empty();
|
||||
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 {
|
||||
sub,
|
||||
posts,
|
||||
|
|
119
src/utils.rs
119
src/utils.rs
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
use crate::config::get_setting;
|
||||
//
|
||||
// CRATES
|
||||
|
@ -156,6 +157,7 @@ impl PollOption {
|
|||
|
||||
// Post flags with nsfw and stickied
|
||||
pub struct Flags {
|
||||
pub spoiler: bool,
|
||||
pub nsfw: bool,
|
||||
pub stickied: bool,
|
||||
}
|
||||
|
@ -167,6 +169,7 @@ pub struct Media {
|
|||
pub width: i64,
|
||||
pub height: i64,
|
||||
pub poster: String,
|
||||
pub download_name: String,
|
||||
}
|
||||
|
||||
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 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(),
|
||||
Self {
|
||||
|
@ -243,6 +255,7 @@ impl Media {
|
|||
width: source["width"].as_i64().unwrap_or_default(),
|
||||
height: source["height"].as_i64().unwrap_or_default(),
|
||||
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
||||
download_name,
|
||||
},
|
||||
gallery,
|
||||
)
|
||||
|
@ -296,6 +309,7 @@ pub struct Post {
|
|||
pub body: String,
|
||||
pub author: Author,
|
||||
pub permalink: String,
|
||||
pub link_title: String,
|
||||
pub poll: Option<Poll>,
|
||||
pub score: (String, String),
|
||||
pub upvote_ratio: i64,
|
||||
|
@ -307,6 +321,7 @@ pub struct Post {
|
|||
pub domain: String,
|
||||
pub rel_time: String,
|
||||
pub created: String,
|
||||
pub created_ts: u64,
|
||||
pub num_duplicates: u64,
|
||||
pub comments: (String, String),
|
||||
pub gallery: Vec<GalleryMedia>,
|
||||
|
@ -338,6 +353,7 @@ impl Post {
|
|||
let data = &post["data"];
|
||||
|
||||
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 ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
let title = val(post, "title");
|
||||
|
@ -384,6 +400,7 @@ impl Post {
|
|||
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||
poster: String::new(),
|
||||
download_name: String::new(),
|
||||
},
|
||||
media,
|
||||
domain: val(post, "domain"),
|
||||
|
@ -402,13 +419,16 @@ impl Post {
|
|||
},
|
||||
},
|
||||
flags: Flags {
|
||||
spoiler: data["spoiler"].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(),
|
||||
},
|
||||
permalink: val(post, "permalink"),
|
||||
link_title: val(post, "link_title"),
|
||||
poll: Poll::parse(&data["poll_data"]),
|
||||
rel_time,
|
||||
created,
|
||||
created_ts,
|
||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
|
||||
gallery,
|
||||
|
@ -417,7 +437,6 @@ impl Post {
|
|||
ws_url: val(post, "websocket_url"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||
}
|
||||
}
|
||||
|
@ -574,9 +593,11 @@ pub struct Preferences {
|
|||
pub front_page: String,
|
||||
pub layout: String,
|
||||
pub wide: String,
|
||||
pub blur_spoiler: String,
|
||||
pub show_nsfw: String,
|
||||
pub blur_nsfw: String,
|
||||
pub hide_hls_notification: String,
|
||||
pub hide_sidebar_and_summary: String,
|
||||
pub use_hls: String,
|
||||
pub autoplay_videos: String,
|
||||
pub fixed_navbar: String,
|
||||
|
@ -610,7 +631,9 @@ impl Preferences {
|
|||
front_page: setting(req, "front_page"),
|
||||
layout: setting(req, "layout"),
|
||||
wide: setting(req, "wide"),
|
||||
blur_spoiler: setting(req, "blur_spoiler"),
|
||||
show_nsfw: setting(req, "show_nsfw"),
|
||||
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
|
||||
blur_nsfw: setting(req, "blur_nsfw"),
|
||||
use_hls: setting(req, "use_hls"),
|
||||
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
|
||||
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 permalink = val(post, "permalink");
|
||||
|
@ -702,6 +727,7 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||
distinguished: val(post, "distinguished"),
|
||||
},
|
||||
permalink,
|
||||
link_title: val(post, "link_title"),
|
||||
poll,
|
||||
score: format_num(score),
|
||||
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(),
|
||||
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||
poster: String::new(),
|
||||
download_name: String::new(),
|
||||
},
|
||||
flair: Flair {
|
||||
flair_parts: FlairPart::parse(
|
||||
|
@ -729,12 +756,14 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||
},
|
||||
},
|
||||
flags: Flags {
|
||||
spoiler: post["data"]["spoiler"].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),
|
||||
},
|
||||
domain: val(post, "domain"),
|
||||
rel_time,
|
||||
created,
|
||||
created_ts,
|
||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
|
||||
gallery,
|
||||
|
@ -875,9 +904,9 @@ pub fn format_url(url: &str) -> String {
|
|||
|
||||
// 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_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 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());
|
||||
|
||||
// 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 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(">>", ">");
|
||||
let image_replacement = format!("<a href=\"{image_url}<img src=\"{image_url}</a>");
|
||||
/* 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.
|
||||
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. "\"" is how the quotes are formatted within image_text beforehand
|
||||
image_caption = image_caption.replace("\\"", "\"");
|
||||
|
||||
_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
|
||||
.replace(&text1, formatted_url)
|
||||
.replace(&image_to_replace, &image_replacement)
|
||||
.replace(&text1, format!("{_preview_type}$2"))
|
||||
.replace(&image_to_replace, &_image_replacement)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
@ -993,7 +1055,7 @@ pub fn redirect(path: &str) -> Response<Body> {
|
|||
|
||||
/// Renders a generic error landing page.
|
||||
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 body = ErrorTemplate {
|
||||
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())
|
||||
}
|
||||
|
||||
// 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)]
|
||||
mod tests {
|
||||
use super::{format_num, format_url, rewrite_urls};
|
||||
|
@ -1164,11 +1240,24 @@ async fn test_fetching_ws() {
|
|||
|
||||
#[test]
|
||||
fn test_rewriting_image_links() {
|
||||
let input = r#"<p><a href="https://preview.redd.it/zq21ggkj2xo31.png?width=2560&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab">https://preview.redd.it/zq21ggkj2xo31.png?width=2560&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab</a></p>
|
||||
<p><a href="https://preview.redd.it/vty9ocij2xo31.png?width=2560&format=png&auto=webp&s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877">https://preview.redd.it/vty9ocij2xo31.png?width=2560&format=png&auto=webp&s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877</a></p>
|
||||
<p><a href="https://preview.redd.it/bdfdxkjj2xo31.png?width=2560&format=png&auto=webp&s=d0fa420ece27605e882e89cb4711d75d774322ac">https://preview.redd.it/bdfdxkjj2xo31.png?width=2560&format=png&auto=webp&s=d0fa420ece27605e882e89cb4711d75d774322ac</a></p>
|
||||
<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>
|
||||
<p><a href="https://preview.redd.it/rbu2ca2b2xo31.png?width=2560&format=png&auto=webp&s=afb538cf784d2e339de9a91aba5dc9c92e47988f">caption 2</a></p>"#;
|
||||
let output = r#"<p><a href="/preview/pre/zq21ggkj2xo31.png?width=2560&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab"><img src="/preview/pre/zq21ggkj2xo31.png?width=2560&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab"></a></p> <p><a href="/preview/pre/vty9ocij2xo31.png?width=2560&format=png&auto=webp&s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877"><img src="/preview/pre/vty9ocij2xo31.png?width=2560&format=png&auto=webp&s=fc7c7ef993a5e9ef656d5f5d9cf8290a0a1df877"></a></p> <p><a href="/preview/pre/bdfdxkjj2xo31.png?width=2560&format=png&auto=webp&s=d0fa420ece27605e882e89cb4711d75d774322ac"><img src="/preview/pre/bdfdxkjj2xo31.png?width=2560&format=png&auto=webp&s=d0fa420ece27605e882e89cb4711d75d774322ac"></a></p> <p><a href="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img src="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a></p> <p><a href="/preview/pre/rbu2ca2b2xo31.png?width=2560&format=png&auto=webp&s=afb538cf784d2e339de9a91aba5dc9c92e47988f"><img src="/preview/pre/rbu2ca2b2xo31.png?width=2560&format=png&auto=webp&s=afb538cf784d2e339de9a91aba5dc9c92e47988f"></a></p>"#;
|
||||
let input =
|
||||
r#"<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>"#;
|
||||
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
|
||||
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("/"), "");
|
||||
}
|
||||
|
|
200
static/style.css
200
static/style.css
|
@ -34,6 +34,7 @@
|
|||
font-family: 'Inter';
|
||||
src: url('/Inter.var.woff2') format('woff2-variations');
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
/* Automatic theme selection */
|
||||
|
@ -51,6 +52,7 @@
|
|||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
--popup: #b80a27;
|
||||
--spoiler: #ddd;
|
||||
|
||||
/* Hint color theme to browser for scrollbar */
|
||||
color-scheme: dark;
|
||||
|
@ -70,6 +72,7 @@
|
|||
--highlighted: white;
|
||||
--visited: #555;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--spoiler: #0f0f0f;
|
||||
|
||||
/* Hint color theme to browser for scrollbar */
|
||||
color-scheme: light;
|
||||
|
@ -187,6 +190,11 @@ nav #redlib {
|
|||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings_link {
|
||||
opacity: 0.8;
|
||||
margin-left: 10px;
|
||||
|
@ -644,7 +652,6 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||
}
|
||||
|
||||
#sort_options + #timeframe:not(#search_sort > #timeframe) {
|
||||
margin-left: 10px;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
|
@ -654,24 +661,17 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||
}
|
||||
|
||||
#search_sort {
|
||||
background: var(--highlighted);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#search_sort > #search {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
#search_sort > *, .search_widget_divider_box > *, .search_widget_divider_box #sort_options {
|
||||
background: var(--highlighted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#search_sort > *, #searchbox > * { font-size: 15px; }
|
||||
|
||||
#search_sort > :not(:first-child), #search_sort > #sort_options {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
border-right: 0;
|
||||
border-left: 2px solid var(--background);
|
||||
box-shadow: none;
|
||||
#search_sort > #search {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
@ -690,6 +690,66 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||
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 {
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
|
@ -923,6 +983,15 @@ a.search_subreddit:hover {
|
|||
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 {
|
||||
max-width: calc(100% - 40px);
|
||||
grid-area: post_media;
|
||||
|
@ -950,11 +1019,25 @@ a.search_subreddit:hover {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
.post_nsfw_blur {
|
||||
.post_blurred img,
|
||||
.post_blurred svg,
|
||||
.post_blurred video {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -979,10 +1062,6 @@ a.search_subreddit:hover {
|
|||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.gallery figcaption {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.gallery .outbound_url {
|
||||
color: var(--accent);
|
||||
text-overflow: ellipsis;
|
||||
|
@ -1010,6 +1089,9 @@ a.search_subreddit:hover {
|
|||
|
||||
.post_body img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.post_poll {
|
||||
|
@ -1094,12 +1176,12 @@ a.search_subreddit:hover {
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#post_links > li.desktop_item {
|
||||
.desktop_item {
|
||||
display: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
#post_links > li.mobile_item {
|
||||
@media screen and (min-width: 481px) {
|
||||
.mobile_item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1131,10 +1213,6 @@ a.search_subreddit:hover {
|
|||
z-index: 0;
|
||||
}
|
||||
|
||||
.thumb_nsfw_blur {
|
||||
filter: blur(0.3rem)
|
||||
}
|
||||
|
||||
.post_thumbnail.no_thumbnail {
|
||||
background-color: var(--highlighted);
|
||||
}
|
||||
|
@ -1187,6 +1265,10 @@ a.search_subreddit:hover {
|
|||
}
|
||||
}
|
||||
|
||||
.comment figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment_left, .comment_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1213,10 +1295,6 @@ a.search_subreddit:hover {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment_subreddit {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment_score {
|
||||
color: var(--accent);
|
||||
background: var(--foreground);
|
||||
|
@ -1235,10 +1313,28 @@ a.search_subreddit:hover {
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment_data > * {
|
||||
.comment:has([id]) .comment_data > * {
|
||||
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 {
|
||||
max-width: 500px;
|
||||
align-self: center;
|
||||
|
@ -1308,6 +1404,15 @@ summary.comment_data {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user_comment_data_divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user_comment_data_divider .dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moderator, .admin { opacity: 1; }
|
||||
.op, .moderator, .admin { font-weight: bold; }
|
||||
|
||||
|
@ -1484,10 +1589,19 @@ input[type="submit"] {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.md > *:not(:first-child) {
|
||||
.md > p:not(:first-child):not(:last-child) {
|
||||
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 h2 { font-size: 20px; }
|
||||
.md h3 { font-size: 18px; }
|
||||
|
@ -1711,8 +1825,23 @@ td, th {
|
|||
.comment_right { padding: 5px 0 10px 2px; }
|
||||
.comment_author { 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; }
|
||||
.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 {
|
||||
min-width: 32px;
|
||||
|
@ -1723,10 +1852,11 @@ td, th {
|
|||
}
|
||||
|
||||
#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 }
|
||||
|
||||
.desktop_item { display: none }
|
||||
.mobile_item { display: auto }
|
||||
|
||||
.popup {
|
||||
width: auto;
|
||||
}
|
||||
|
|
14
static/themes/icebergDark.css
Normal file
14
static/themes/icebergDark.css
Normal 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);
|
||||
}
|
|
@ -2,10 +2,14 @@
|
|||
{% block title %}Error: {{ msg }}{% endblock %}
|
||||
{% block sortstyle %}{% endblock %}
|
||||
{% block content %}
|
||||
<div id="error">
|
||||
<h1>{{ msg }}</h1>
|
||||
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
||||
<br />
|
||||
<h3>Head back <a href="/">home</a>?</h3>
|
||||
</div>
|
||||
<div id="error">
|
||||
<h1>{{ msg }}</h1>
|
||||
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
||||
<br />
|
||||
<h3>Expected something to work? <a
|
||||
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 %}
|
|
@ -10,25 +10,34 @@
|
|||
{% block content %}
|
||||
<div id="column_one">
|
||||
<form id="search_sort">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
||||
{% if sub != "" %}
|
||||
<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 class="search_widget_divider_box">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
||||
<div class="search_widget_divider_box">
|
||||
{% if sub != "" %}
|
||||
<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>
|
||||
{% 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 %}<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if !is_filtered %}
|
||||
|
|
|
@ -54,6 +54,11 @@
|
|||
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||
</select>
|
||||
</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() %}
|
||||
<div class="prefs-group">
|
||||
<label for="show_nsfw">Show NSFW posts:</label>
|
||||
|
@ -71,11 +76,16 @@
|
|||
<input type="hidden" value="off" name="autoplay_videos">
|
||||
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<div class="prefs-group">
|
||||
<label for="fixed_navbar">Keep navbar fixed</label>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</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">
|
||||
<label for="use_hls">Use HLS for videos</label>
|
||||
<details id="feeds">
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
</footer>
|
||||
</div>
|
||||
{% 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>
|
||||
{% if is_filtered %}
|
||||
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
{% else if !post.title.is_empty() %}
|
||||
{% call utils::post_in_list(post) %}
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment user-comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score" title="{{ post.score.1 }}">
|
||||
{% if prefs.hide_score != "on" %}
|
||||
|
@ -63,8 +63,13 @@
|
|||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_link" href="{{ post.permalink }}">Comment on r/{{ post.community }}</a>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
<a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a>
|
||||
<div class="user_comment_data_divider">
|
||||
<span class="created-in"> in </span>
|
||||
<a class="comment_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}"> {{ post.rel_time }}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body|safe }}</p>
|
||||
</details>
|
||||
|
|
|
@ -62,8 +62,9 @@
|
|||
{%- endmacro %}
|
||||
|
||||
{% macro post(post) -%}
|
||||
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
|
||||
<!-- POST CONTENT -->
|
||||
<div class="post highlighted">
|
||||
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
|
@ -93,6 +94,7 @@
|
|||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
|
||||
</h1>
|
||||
|
||||
<!-- POST MEDIA -->
|
||||
|
@ -101,7 +103,7 @@
|
|||
<div class="post_media_content">
|
||||
<a href="{{ post.media.url }}" class="post_media_image" >
|
||||
{% 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 }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
|
@ -161,13 +163,28 @@
|
|||
<span class="label"> Upvotes</span></div>
|
||||
<div class="post_footer">
|
||||
<ul id="post_links">
|
||||
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
|
||||
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li>
|
||||
<li>
|
||||
<a href="{{ post.permalink }}">
|
||||
<span class="desktop_item">perma</span>link
|
||||
</a>
|
||||
</li>
|
||||
{% if post.num_duplicates > 0 %}
|
||||
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
|
||||
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
|
||||
<li>
|
||||
<a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
|
||||
dup<span class="desktop_item">licat</span>es
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
|
||||
</div>
|
||||
|
@ -175,8 +192,7 @@
|
|||
{%- endmacro %}
|
||||
|
||||
{% macro external_reddit_link(permalink) %}
|
||||
{% for dev_type in ["desktop", "mobile"] %}
|
||||
<li class="{{ dev_type }}_item">
|
||||
<li>
|
||||
<a
|
||||
{% if prefs.disable_visit_reddit_confirmation != "on" %}
|
||||
href="#popup"
|
||||
|
@ -190,11 +206,11 @@
|
|||
{% call visit_reddit_confirmation(permalink) %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% 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">
|
||||
{% let community -%}
|
||||
{% if post.community.starts_with("u_") -%}
|
||||
|
@ -222,7 +238,7 @@
|
|||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
|
||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% 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>
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% 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 }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
|
||||
width="{{ post.media.width }}px"
|
||||
height="{{ post.media.height }}px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
@ -245,26 +260,22 @@
|
|||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
|
||||
<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" %}
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && (post.post_type == "gif" || post.post_type == "video") %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||
<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.url }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
|
||||
{% endif %}
|
||||
{% 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() %}
|
||||
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Thumbnail</title>
|
||||
|
@ -272,7 +283,7 @@
|
|||
</svg>
|
||||
{% else %}
|
||||
<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 }}"/>
|
||||
<desc>
|
||||
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue