mirror of
https://github.com/redlib-org/redlib.git
synced 2025-06-04 05:40:22 +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
|
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
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.*"
|
- "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 }}
|
||||||
|
|
16
.github/workflows/main-rust.yml
vendored
16
.github/workflows/main-rust.yml
vendored
|
@ -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
6
.gitignore
vendored
|
@ -1,4 +1,10 @@
|
||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
|
redlib.toml
|
||||||
|
|
||||||
# Idea Files
|
# Idea Files
|
||||||
.idea/
|
.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"
|
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"
|
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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
20
README.md
20
README.md
|
@ -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` |
|
||||||
|
|
6
app.json
6
app.json
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
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 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
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::{
|
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]
|
||||||
|
|
|
@ -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] = &[""];
|
||||||
|
|
|
@ -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(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
119
src/utils.rs
119
src/utils.rs
|
@ -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. "\"" 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
|
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&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab">https://preview.redd.it/zq21ggkj2xo31.png?width=2560&format=png&auto=webp&s=539d8050628ec1190cac26468fe99cc66b6071ab</a></p>
|
let input =
|
||||||
<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>
|
r#"<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/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>
|
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"#;
|
||||||
<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>"#;
|
|
||||||
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("/"), "");
|
||||||
|
}
|
||||||
|
|
200
static/style.css
200
static/style.css
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
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 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 %}
|
|
@ -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") %}
|
→
|
||||||
</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" />
|
|
||||||
→
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if !is_filtered %}
|
{% if !is_filtered %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"> 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>
|
</summary>
|
||||||
<p class="comment_body">{{ post.body|safe }}</p>
|
<p class="comment_body">{{ post.body|safe }}</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -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">•</span>
|
<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>
|
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 }}"/>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue