Compare commits
60 Commits
b076b076e3
...
v0.35.3
Author | SHA1 | Date | |
---|---|---|---|
1c1e627815 | |||
4b854eb84d | |||
712790acbe | |||
062d810aad | |||
64eec64ebe | |||
3ff907d6c1 | |||
f4a457e529 | |||
7dda8d9bbb | |||
b99412b4a1 | |||
1838fdaea4 | |||
f71b0cd178 | |||
604db902e9 | |||
e57eaa0b78 | |||
31ad8c5f7b | |||
5aef97410c | |||
8d0ed4682e | |||
fe4fed0504 | |||
6e2e679a0e | |||
6b44c1abf2 | |||
a807002ddf | |||
403513ac4c | |||
72f7d9d08c | |||
e6273e2ed5 | |||
f1d4e6a417 | |||
e0d7837c02 | |||
2d6ac78acf | |||
1e54c639d3 | |||
d5f137ce47 | |||
245fd9d408 | |||
b54620b5aa | |||
69c7a69afd | |||
2991813c2d | |||
7156be6ad0 | |||
f9b1a832c4 | |||
1ffbce1ad8 | |||
793047f63f | |||
3625fdfdbe | |||
28f85f2599 | |||
7be29f609c | |||
f18d135045 | |||
f2c410f9ea | |||
8ef45456d6 | |||
118708400a | |||
28e72c9058 | |||
0b15250cc8 | |||
408ebe6ef1 | |||
7a0ea1fbd3 | |||
db8b92ea55 | |||
c494fbec31 | |||
438e412be3 | |||
d0e081e6a0 | |||
041ecceeaf | |||
3677ca10e2 | |||
21fd34710c | |||
4205959ade | |||
27b56c1781 | |||
374238abc3 | |||
9bdb5c8966 | |||
410872d988 | |||
c890e809b7 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
target
|
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@ -1,2 +1,3 @@
|
||||
liberapay: spike
|
||||
custom: ['https://www.buymeacoffee.com/spikecodes']
|
||||
liberapay: sigaloid
|
||||
buy_me_a_coffee: sigaloid
|
||||
github: sigaloid
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE FILING A BUG REPORT: Ensure that you are running the latest git commit. Visit /info on your instance, and ensure the git commit listed is the same commit listed on the home page.
|
||||
-->
|
||||
|
||||
## Describe the bug
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
@ -31,3 +35,7 @@ Steps to reproduce the behavior:
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
||||
|
||||
<!-- Mandatory -->
|
||||
- [ ] I checked that the instance that this was reported on is running the latest git commit, or I can reproduce it locally on the latest git commit
|
75
.github/workflows/main-docker.yml
vendored
75
.github/workflows/main-docker.yml
vendored
@ -15,15 +15,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { platform: linux/amd64, target: x86_64-unknown-linux-musl}
|
||||
- { platform: linux/arm64, target: aarch64-unknown-linux-musl}
|
||||
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf}
|
||||
- { platform: linux/amd64, target: x86_64-unknown-linux-musl }
|
||||
- { platform: linux/arm64, target: aarch64-unknown-linux-musl }
|
||||
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf }
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
@ -31,21 +29,17 @@ jobs:
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
-
|
||||
name: Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Quay.io Container Registry
|
||||
- name: Login to Quay.io Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@ -55,17 +49,15 @@ jobs:
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
file: Dockerfile
|
||||
build-args: TARGET=${{ matrix.target }}
|
||||
-
|
||||
name: Export digest
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
-
|
||||
name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests
|
||||
name: digests-${{ matrix.target }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@ -74,17 +66,16 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
@ -92,31 +83,27 @@ jobs:
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
-
|
||||
name: Login to Quay.io Container Registry
|
||||
- name: Login to Quay.io Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Create manifest list and push
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Push README to Quay.io
|
||||
uses: christian-korneck/update-container-description-action@v1
|
||||
env:
|
||||
DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
|
||||
with:
|
||||
destination_container_repo: quay.io/redlib/redlib
|
||||
provider: quay
|
||||
readme_file: 'README.md'
|
||||
# - name: Push README to Quay.io
|
||||
# uses: christian-korneck/update-container-description-action@v1
|
||||
# env:
|
||||
# DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
|
||||
# with:
|
||||
# destination_container_repo: quay.io/redlib/redlib
|
||||
# provider: quay
|
||||
# readme_file: 'README.md'
|
||||
|
||||
-
|
||||
name: Inspect image
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
|
2
.github/workflows/main-rust.yml
vendored
2
.github/workflows/main-rust.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
||||
- name: Calculate SHA256 checksum
|
||||
run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload a Build Artifact
|
||||
with:
|
||||
name: redlib
|
||||
|
476
Cargo.lock
generated
476
Cargo.lock
generated
@ -30,6 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
@ -78,42 +79,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -124,7 +97,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atom_syndication"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder 0.20.0",
|
||||
"diligent-date-parser",
|
||||
"never",
|
||||
"quick-xml 0.31.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -148,6 +134,12 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@ -171,9 +163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "6.0.0"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
|
||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@ -223,7 +215,7 @@ dependencies = [
|
||||
"cached_proc_macro",
|
||||
"cached_proc_macro_types",
|
||||
"futures",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"instant",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
@ -236,10 +228,10 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.9",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -260,6 +252,15 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.7"
|
||||
@ -348,14 +349,38 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"darling_macro 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.9",
|
||||
"darling_macro 0.20.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -368,8 +393,19 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -378,9 +414,9 @@ version = "0.20.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.9",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -398,6 +434,68 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
|
||||
dependencies = [
|
||||
"derive_builder_macro 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
|
||||
dependencies = [
|
||||
"derive_builder_macro 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
|
||||
dependencies = [
|
||||
"darling 0.14.4",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
|
||||
dependencies = [
|
||||
"darling 0.20.9",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
|
||||
dependencies = [
|
||||
"derive_builder_core 0.12.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
|
||||
dependencies = [
|
||||
"derive_builder_core 0.20.0",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -408,12 +506,30 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diligent-date-parser"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@ -613,6 +729,12 @@ dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -685,9 +807,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.25.0"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070"
|
||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
@ -695,7 +817,6 @@ dependencies = [
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
@ -723,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -735,6 +856,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.12"
|
||||
@ -778,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
@ -862,6 +989,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "never"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@ -878,6 +1011,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
@ -912,6 +1054,18 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "once_map"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown 0.15.0",
|
||||
"parking_lot",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
@ -1002,6 +1156,26 @@ version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
@ -1047,11 +1221,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redsunlib"
|
||||
version = "0.35.2"
|
||||
version = "0.35.3"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"askama",
|
||||
"base64",
|
||||
"async-recursion",
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
"build_html",
|
||||
"cached",
|
||||
@ -1069,11 +1243,14 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pretty_env_logger",
|
||||
"regex",
|
||||
"rinja",
|
||||
"route-recognizer",
|
||||
"rss",
|
||||
"rust-embed",
|
||||
"sealed_test",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_path",
|
||||
"serde_yaml",
|
||||
"time",
|
||||
"tokio",
|
||||
@ -1126,6 +1303,43 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"rinja_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja_derive"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"once_map",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rinja_parser",
|
||||
"rustc-hash",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rinja_parser"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
@ -1138,6 +1352,18 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||
|
||||
[[package]]
|
||||
name = "rss"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
|
||||
dependencies = [
|
||||
"atom_syndication",
|
||||
"derive_builder 0.12.0",
|
||||
"never",
|
||||
"quick-xml 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.4.0"
|
||||
@ -1158,7 +1384,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -1179,6 +1405,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
@ -1194,55 +1426,44 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.22.4"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.0"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"rustls-pki-types",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.4"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
@ -1288,6 +1509,16 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sealed_test"
|
||||
version = "1.1.0"
|
||||
@ -1307,7 +1538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1350,7 +1581,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1364,6 +1595,59 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_path_core",
|
||||
"serde_json_path_macros",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_macros"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde_json_path_core",
|
||||
"serde_json_path_macros_internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_macros_internal"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
@ -1437,6 +1721,18 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@ -1444,10 +1740,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
@ -1498,7 +1799,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1576,17 +1877,16 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.25.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@ -1950,11 +2250,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
14
Cargo.toml
14
Cargo.toml
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "redsunlib"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://git.stardust.wtf/iridium/redsunlib"
|
||||
version = "0.35.2"
|
||||
version = "0.35.3"
|
||||
authors = [
|
||||
"Matthew Esposito <matt+cargo@matthew.science>",
|
||||
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
||||
@ -11,7 +11,7 @@ authors = [
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12.1", default-features = false }
|
||||
rinja = { version = "0.3.4", default-features = false }
|
||||
cached = { version = "0.51.3", features = ["async"] }
|
||||
clap = { version = "4.4.11", default-features = false, features = [
|
||||
"std",
|
||||
@ -22,7 +22,7 @@ serde = { version = "1.0.193", features = ["derive"] }
|
||||
cookie = "0.18.0"
|
||||
futures-lite = "2.2.0"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
hyper-rustls = "0.25.0"
|
||||
hyper-rustls = "0.24.2"
|
||||
percent-encoding = "2.3.1"
|
||||
route-recognizer = "0.3.1"
|
||||
serde_json = "1.0.108"
|
||||
@ -31,7 +31,7 @@ time = { version = "0.3.31", features = ["local-offset"] }
|
||||
url = "2.5.0"
|
||||
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
||||
libflate = "2.0.0"
|
||||
brotli = { version = "6.0.0", features = ["std"] }
|
||||
brotli = { version = "7.0.0", features = ["std"] }
|
||||
toml = "0.8.8"
|
||||
once_cell = "1.19.0"
|
||||
serde_yaml = "0.9.29"
|
||||
@ -42,7 +42,11 @@ fastrand = "2.0.1"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5.0"
|
||||
dotenvy = "0.15.7"
|
||||
rss = "2.0.7"
|
||||
arc-swap = "1.7.1"
|
||||
serde_json_path = "0.6.7"
|
||||
async-recursion = "1.1.1"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
lipsum = "0.9.0"
|
||||
|
@ -71,7 +71,7 @@ And at the time, I was reading an excerpt from Mao Zedong, so the name seemed ap
|
||||
|
||||
- [Rust](https://www.rust-lang.org/) - Programming language
|
||||
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
|
||||
- [Askama](https://github.com/djc/askama) - Templating engine
|
||||
- [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
|
||||
- [Rustls](https://github.com/rustls/rustls) - TLS library
|
||||
|
||||
## How is it different from other Reddit front ends?
|
||||
@ -312,14 +312,15 @@ Assign a default value for each instance-specific setting by passing environment
|
||||
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
|
||||
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
|
||||
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
|
||||
|
||||
| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
|
||||
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
|
||||
## Default user settings
|
||||
|
||||
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
|
||||
|
||||
| Name | Possible values | Default value |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
|
||||
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
|
||||
@ -337,4 +338,5 @@ Assign a default value for each user-modifiable setting by passing environment v
|
||||
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
|
||||
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
|
||||
| `HIDE_SCORE` | `["on", "off"]` | `off` |
|
||||
| `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
|
||||
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
|
||||
|
8
app.json
8
app.json
@ -62,7 +62,7 @@
|
||||
"REDLIB_BANNER": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
|
||||
@ -76,6 +76,12 @@
|
||||
},
|
||||
"REDLIB_PUSHSHIFT_FRONTEND": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_ENABLE_RSS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_FULL_URL": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@ PORT=12345
|
||||
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
||||
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)
|
||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=(sub1+sub2+sub3)
|
||||
#REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||
#REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
||||
|
@ -30,7 +30,8 @@ RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service ~@privileged ~@resources
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
|
135
src/client.rs
135
src/client.rs
@ -22,15 +22,16 @@ use crate::server::RequestExt;
|
||||
use crate::utils::format_url;
|
||||
|
||||
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||
const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
|
||||
|
||||
const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
|
||||
const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
|
||||
|
||||
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
|
||||
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
|
||||
|
||||
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.expect("No native root certificates found")
|
||||
.https_only()
|
||||
.enable_http1()
|
||||
.build();
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||
client::Client::builder().build(https)
|
||||
});
|
||||
|
||||
@ -44,6 +45,11 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
|
||||
|
||||
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
static URL_PAIRS: [(&str, &str); 2] = [
|
||||
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
|
||||
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
|
||||
];
|
||||
|
||||
/// Gets the canonical path for a resource on Reddit. This is accomplished by
|
||||
/// making a `HEAD` request to Reddit at the path given in `path`.
|
||||
///
|
||||
@ -57,13 +63,32 @@ pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
||||
/// `Location` header. An `Err(String)` is returned if Reddit responds with a
|
||||
/// 429, or if we were unable to decode the value in the `Location` header.
|
||||
#[cached(size = 1024, time = 600, result = true)]
|
||||
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
let res = reddit_head(path.clone(), true).await?;
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
|
||||
if tries == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// for each URL pair, try the HEAD request
|
||||
let res = {
|
||||
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
|
||||
let mut res = None;
|
||||
for (url_base, url_base_host) in URL_PAIRS {
|
||||
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
|
||||
if let Some(res) = &res {
|
||||
if !res.status().is_client_error() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
};
|
||||
|
||||
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
|
||||
let status = res.status().as_u16();
|
||||
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
|
||||
|
||||
match status {
|
||||
429 => Err("Too many requests.".to_string()),
|
||||
|
||||
// If Reddit responds with a 2xx, then the path is already canonical.
|
||||
200..=299 => Ok(Some(path)),
|
||||
|
||||
@ -73,6 +98,7 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
let Ok(original) = val.to_str() else {
|
||||
return Err("Unable to decode Location header.".to_string());
|
||||
};
|
||||
|
||||
// We need to strip the .json suffix from the original path.
|
||||
// In addition, we want to remove share parameters.
|
||||
// Cut it off here instead of letting it propagate all the way
|
||||
@ -85,7 +111,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
// also remove all Reddit domain parts with format_url.
|
||||
// Otherwise, it will literally redirect to Reddit.com.
|
||||
let uri = format_url(stripped_uri);
|
||||
Ok(Some(uri))
|
||||
|
||||
// Decrement tries and try again
|
||||
canonical_path(uri, tries - 1).await
|
||||
}
|
||||
None => Ok(None),
|
||||
},
|
||||
@ -94,6 +122,12 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
// as above), return a None.
|
||||
300..=399 => Ok(None),
|
||||
|
||||
// Rate limiting
|
||||
429 => Err("Too many requests.".to_string()),
|
||||
|
||||
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
|
||||
403 if policy_error => Err("Too many requests.".to_string()),
|
||||
|
||||
_ => Ok(
|
||||
res
|
||||
.headers()
|
||||
@ -160,20 +194,26 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
|
||||
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
|
||||
/// 3xx codes Reddit returns and will automatically redirect.
|
||||
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::GET, path, true, quarantine)
|
||||
request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
|
||||
}
|
||||
|
||||
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
||||
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::HEAD, path, false, quarantine)
|
||||
/// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
|
||||
fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
||||
request(&Method::HEAD, path, false, quarantine, base_path, host)
|
||||
}
|
||||
|
||||
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
||||
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
// request(&Method::HEAD, path, false, quarantine, false)
|
||||
// }
|
||||
// Unused - reddit_head is only ever called in the context of a short URL
|
||||
|
||||
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
|
||||
/// will recurse on the URL that Reddit provides in the Location HTTP header
|
||||
/// in its response.
|
||||
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
||||
// Build Reddit URL from path.
|
||||
let url = format!("{REDDIT_URL_BASE}{path}");
|
||||
let url = format!("{base_path}{path}");
|
||||
|
||||
// Construct the hyper client from the HTTPS connector.
|
||||
let client: Client<_, Body> = CLIENT.clone();
|
||||
@ -198,7 +238,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
.header("Client-Vendor-Id", vendor_id)
|
||||
.header("X-Reddit-Device-Id", device_id)
|
||||
.header("x-reddit-loid", loid)
|
||||
.header("Host", "oauth.reddit.com")
|
||||
.header("Host", host)
|
||||
.header("Authorization", &format!("Bearer {token}"))
|
||||
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
@ -253,6 +293,8 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
.to_string(),
|
||||
true,
|
||||
quarantine,
|
||||
base_path,
|
||||
host,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
@ -374,6 +416,16 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
match serde_json::from_reader(body.reader()) {
|
||||
Ok(value) => {
|
||||
let json: Value = value;
|
||||
|
||||
// If user is suspended
|
||||
if let Some(data) = json.get("data") {
|
||||
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
|
||||
if is_suspended {
|
||||
return Err("suspended".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If Reddit returned an error
|
||||
if json["error"].is_i64() {
|
||||
// OAuth token has expired; http status 401
|
||||
@ -382,6 +434,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
let () = force_refresh_token().await;
|
||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
||||
}
|
||||
|
||||
// Handle quarantined
|
||||
if json["reason"] == "quarantined" {
|
||||
return Err("quarantined".into());
|
||||
}
|
||||
// Handle gated
|
||||
if json["reason"] == "gated" {
|
||||
return Err("gated".into());
|
||||
}
|
||||
// Handle private subs
|
||||
if json["reason"] == "private" {
|
||||
return Err("private".into());
|
||||
}
|
||||
// Handle banned subs
|
||||
if json["reason"] == "banned" {
|
||||
return Err("banned".into());
|
||||
}
|
||||
|
||||
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
|
||||
} else {
|
||||
Ok(json)
|
||||
@ -417,13 +487,34 @@ async fn test_localization_popular() {
|
||||
async fn test_obfuscated_share_link() {
|
||||
let share_link = "/r/rust/s/kPgq8WNHRK".into();
|
||||
// Correct link without share parameters
|
||||
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into();
|
||||
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link)));
|
||||
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
|
||||
assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_share_link_strip_json() {
|
||||
let link = "/17krzvz".into();
|
||||
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into();
|
||||
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link)));
|
||||
let canonical_link = "/comments/17krzvz".into();
|
||||
assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_private_sub() {
|
||||
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("private".into()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_banned_sub() {
|
||||
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("banned".into()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_gated_sub() {
|
||||
// quarantine to false to specifically catch when we _don't_ catch it
|
||||
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
|
||||
assert!(link.is_err());
|
||||
assert_eq!(link, Err("gated".into()));
|
||||
}
|
||||
|
@ -111,6 +111,12 @@ pub struct Config {
|
||||
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
|
||||
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
|
||||
pub(crate) pushshift: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_ENABLE_RSS")]
|
||||
pub(crate) enable_rss: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_FULL_URL")]
|
||||
pub(crate) full_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -158,6 +164,8 @@ impl Config {
|
||||
banner: parse("REDLIB_BANNER"),
|
||||
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
|
||||
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
|
||||
enable_rss: parse("REDLIB_ENABLE_RSS"),
|
||||
full_url: parse("REDLIB_FULL_URL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -187,6 +195,8 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
|
||||
"REDLIB_BANNER" => config.banner.clone(),
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
|
||||
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
|
||||
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
|
||||
"REDLIB_FULL_URL" => config.full_url.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ use crate::server::RequestExt;
|
||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
|
||||
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use serde_json::Value;
|
||||
use std::borrow::ToOwned;
|
||||
use std::collections::HashSet;
|
||||
|
@ -3,10 +3,10 @@ use crate::{
|
||||
server::RequestExt,
|
||||
utils::{ErrorTemplate, Preferences},
|
||||
};
|
||||
use askama::Template;
|
||||
use build_html::{Container, Html, HtmlContainer, Table};
|
||||
use hyper::{http::Error, Body, Request, Response};
|
||||
use once_cell::sync::Lazy;
|
||||
use rinja::Template;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@ -126,6 +126,8 @@ impl InstanceInfo {
|
||||
["Compile mode", &self.compile_mode],
|
||||
["SFW only", &convert(&self.config.sfw_only)],
|
||||
["Pushshift frontend", &convert(&self.config.pushshift)],
|
||||
["RSS enabled", &convert(&self.config.enable_rss)],
|
||||
["Full URL", &convert(&self.config.full_url)],
|
||||
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
|
||||
])
|
||||
.with_header_row(["Settings"]),
|
||||
@ -167,6 +169,8 @@ impl InstanceInfo {
|
||||
Compile mode: {}\n
|
||||
SFW only: {:?}\n
|
||||
Pushshift frontend: {:?}\n
|
||||
RSS enabled: {:?}\n
|
||||
Full URL: {:?}\n
|
||||
Config:\n
|
||||
Banner: {:?}\n
|
||||
Hide awards: {:?}\n
|
||||
@ -193,6 +197,8 @@ impl InstanceInfo {
|
||||
self.deploy_unix_ts,
|
||||
self.compile_mode,
|
||||
self.config.sfw_only,
|
||||
self.config.enable_rss,
|
||||
self.config.full_url,
|
||||
self.config.pushshift,
|
||||
self.config.banner,
|
||||
self.config.default_hide_awards,
|
||||
|
11
src/main.rs
11
src/main.rs
@ -284,6 +284,9 @@ async fn main() {
|
||||
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||
app
|
||||
.at("/emote/:subreddit_id/:filename")
|
||||
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
|
||||
app
|
||||
.at("/preview/:loc/award_images/:fullname/:id")
|
||||
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
|
||||
@ -299,6 +302,7 @@ async fn main() {
|
||||
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
||||
|
||||
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
|
||||
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
|
||||
app.at("/user/:name").get(|r| user::profile(r).boxed());
|
||||
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
|
||||
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
|
||||
@ -313,6 +317,9 @@ async fn main() {
|
||||
// Mascots
|
||||
app.at("/mascot/:name").get(|r| mascot_image(r).boxed());
|
||||
|
||||
// RSS Subscriptions
|
||||
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());
|
||||
|
||||
// Subreddit services
|
||||
app
|
||||
.at("/r/:sub")
|
||||
@ -385,7 +392,7 @@ async fn main() {
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
match req.param("id").as_deref() {
|
||||
// Share link
|
||||
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).await {
|
||||
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
|
||||
Ok(Some(path)) => Ok(redirect(&path)),
|
||||
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||
Err(e) => error(req, &e).await,
|
||||
@ -404,7 +411,7 @@ async fn main() {
|
||||
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
|
||||
|
||||
// Short link for post
|
||||
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}")).await {
|
||||
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
|
||||
Ok(path_opt) => match path_opt {
|
||||
Some(path) => Ok(redirect(&path)),
|
||||
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||
|
44
src/oauth.rs
44
src/oauth.rs
@ -6,13 +6,14 @@ use crate::{
|
||||
};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hyper::{client, Body, Method, Request};
|
||||
use log::{info, trace};
|
||||
use log::{error, info, trace};
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::time::{error::Elapsed, timeout};
|
||||
|
||||
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
|
||||
|
||||
static AUTH_ENDPOINT: &str = "https://accounts.reddit.com";
|
||||
static AUTH_ENDPOINT: &str = "https://www.reddit.com";
|
||||
|
||||
// Spoofed client for Android devices
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@ -25,11 +26,32 @@ pub struct Oauth {
|
||||
}
|
||||
|
||||
impl Oauth {
|
||||
/// Create a new OAuth client
|
||||
pub(crate) async fn new() -> Self {
|
||||
let mut oauth = Self::default();
|
||||
oauth.login().await;
|
||||
oauth
|
||||
// Call new_internal until it succeeds
|
||||
loop {
|
||||
let attempt = Self::new_with_timeout().await;
|
||||
match attempt {
|
||||
Ok(Some(oauth)) => {
|
||||
info!("[✅] Successfully created OAuth client");
|
||||
return oauth;
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Failed to create OAuth client. Retrying in 5 seconds...");
|
||||
continue;
|
||||
}
|
||||
Err(duration) => {
|
||||
error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_with_timeout() -> Result<Option<Self>, Elapsed> {
|
||||
let mut oauth = Self::default();
|
||||
timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth))
|
||||
}
|
||||
|
||||
pub(crate) fn default() -> Self {
|
||||
// Generate a device to spoof
|
||||
let device = Device::new();
|
||||
@ -46,7 +68,7 @@ impl Oauth {
|
||||
}
|
||||
async fn login(&mut self) -> Option<()> {
|
||||
// Construct URL for OAuth token
|
||||
let url = format!("{AUTH_ENDPOINT}/api/access_token");
|
||||
let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid");
|
||||
let mut builder = Request::builder().method(Method::POST).uri(&url);
|
||||
|
||||
// Add headers from spoofed client
|
||||
@ -69,13 +91,19 @@ impl Oauth {
|
||||
// Build request
|
||||
let request = builder.body(body).unwrap();
|
||||
|
||||
trace!("Sending token request...");
|
||||
|
||||
// Send request
|
||||
let client: client::Client<_, Body> = CLIENT.clone();
|
||||
let resp = client.request(request).await.ok()?;
|
||||
|
||||
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));
|
||||
|
||||
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
|
||||
// Technically it's not needed, but it's easy for Reddit API to check for this.
|
||||
// It's some kind of header that uniquely identifies the device.
|
||||
// Not worried about the privacy implications, since this is randomly changed
|
||||
// and really only as privacy-concerning as the OAuth token itself.
|
||||
if let Some(header) = resp.headers().get("x-reddit-loid") {
|
||||
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
|
||||
}
|
||||
@ -85,10 +113,14 @@ impl Oauth {
|
||||
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
|
||||
}
|
||||
|
||||
trace!("Serializing response...");
|
||||
|
||||
// Serialize response
|
||||
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
|
||||
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
|
||||
|
||||
trace!("Accessing relevant fields...");
|
||||
|
||||
// Save token and expiry
|
||||
self.token = json.get("access_token")?.as_str()?.to_string();
|
||||
self.expires_in = json.get("expires_in")?.as_u64()?;
|
||||
|
14
src/post.rs
14
src/post.rs
@ -4,14 +4,14 @@ use crate::config::get_setting;
|
||||
use crate::server::RequestExt;
|
||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||
use crate::utils::{
|
||||
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
||||
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_emotes, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
||||
};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use askama::Template;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use rinja::Template;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -72,11 +72,15 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
|
||||
}
|
||||
|
||||
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||
let query_body = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let query_string = format!("q={query_body}&type=comment");
|
||||
let form = url::form_urlencoded::parse(query_string.as_bytes()).collect::<HashMap<_, _>>();
|
||||
let query = form.get("q").unwrap().clone().to_string();
|
||||
|
||||
let comments = match query.as_str() {
|
||||
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
|
||||
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
|
||||
@ -174,7 +178,7 @@ fn build_comment(
|
||||
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
||||
)
|
||||
} else {
|
||||
rewrite_urls(&val(comment, "body_html"))
|
||||
rewrite_emotes(&data["media_metadata"], val(comment, "body_html"))
|
||||
};
|
||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||
|
||||
|
@ -5,10 +5,10 @@ use crate::{
|
||||
subreddit::{can_access_quarantine, quarantine},
|
||||
RequestExt,
|
||||
};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rinja::Template;
|
||||
|
||||
// STRUCTS
|
||||
struct SearchParams {
|
||||
@ -60,7 +60,8 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||
let uri_path = req.uri().path().replace("+", "%2B");
|
||||
let path = format!("{}.json?{}{}&raw_json=1", uri_path, req.uri().query().unwrap_or_default(), nsfw_results);
|
||||
let mut query = param(&path, "q").unwrap_or_default();
|
||||
query = REDDIT_URL_MATCH.replace(&query, "").to_string();
|
||||
|
||||
@ -68,10 +69,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
return Ok(redirect("/"));
|
||||
}
|
||||
|
||||
if query.starts_with("r/") {
|
||||
if query.starts_with("r/") || query.starts_with("user/") {
|
||||
return Ok(redirect(&format!("/{query}")));
|
||||
}
|
||||
|
||||
if query.starts_with("u/") {
|
||||
return Ok(redirect(&format!("/user{}", &query[1..])));
|
||||
}
|
||||
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
let quarantined = can_access_quarantine(&req, &sub);
|
||||
// Handle random subreddits
|
||||
|
@ -3,10 +3,10 @@ use std::collections::HashMap;
|
||||
// CRATES
|
||||
use crate::server::ResponseExt;
|
||||
use crate::utils::{redirect, template, Preferences};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use futures_lite::StreamExt;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
|
@ -1,11 +1,12 @@
|
||||
use crate::{config, utils};
|
||||
// CRATES
|
||||
use crate::utils::{
|
||||
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
||||
};
|
||||
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@ -459,8 +460,71 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
|
||||
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
|
||||
}
|
||||
|
||||
use hyper::header::CONTENT_TYPE;
|
||||
use rss::{ChannelBuilder, Item};
|
||||
|
||||
// Get subreddit
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
||||
|
||||
// Get path
|
||||
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
|
||||
|
||||
// Get subreddit data
|
||||
let subreddit = subreddit(&sub, false).await?;
|
||||
|
||||
// Get posts
|
||||
let (posts, _) = Post::fetch(&path, false).await?;
|
||||
|
||||
// Build the RSS feed
|
||||
let channel = ChannelBuilder::default()
|
||||
.title(&subreddit.title)
|
||||
.description(&subreddit.description)
|
||||
.items(
|
||||
posts
|
||||
.into_iter()
|
||||
.map(|post| Item {
|
||||
title: Some(post.title.to_string()),
|
||||
link: Some(utils::get_post_url(&post)),
|
||||
author: Some(post.author.name),
|
||||
content: Some(rewrite_urls(&post.body)),
|
||||
description: Some(format!(
|
||||
"<a href='{}{}'>Comments</a>",
|
||||
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
|
||||
post.permalink
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.build();
|
||||
|
||||
// Serialize the feed to RSS
|
||||
let body = channel.to_string().into_bytes();
|
||||
|
||||
// Create the HTTP response
|
||||
let mut res = Response::new(Body::from(body));
|
||||
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_fetching_subreddit() {
|
||||
let subreddit = subreddit("rust", false).await;
|
||||
assert!(subreddit.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_gated_and_quarantined() {
|
||||
let quarantined = subreddit("edgy", true).await;
|
||||
assert!(quarantined.is_ok());
|
||||
let gated = subreddit("drugs", true).await;
|
||||
assert!(gated.is_ok());
|
||||
}
|
||||
|
53
src/user.rs
53
src/user.rs
@ -2,8 +2,9 @@
|
||||
use crate::client::json;
|
||||
use crate::server::RequestExt;
|
||||
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
|
||||
use askama::Template;
|
||||
use crate::{config, utils};
|
||||
use hyper::{Body, Request, Response};
|
||||
use rinja::Template;
|
||||
use time::{macros::format_description, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
@ -129,6 +130,56 @@ async fn user(name: &str) -> Result<User, String> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
|
||||
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
|
||||
}
|
||||
use crate::utils::rewrite_urls;
|
||||
use hyper::header::CONTENT_TYPE;
|
||||
use rss::{ChannelBuilder, Item};
|
||||
|
||||
// Get user
|
||||
let user_str = req.param("name").unwrap_or_default();
|
||||
|
||||
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
|
||||
|
||||
// Get path
|
||||
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);
|
||||
|
||||
// Get user
|
||||
let user_obj = user(&user_str).await.unwrap_or_default();
|
||||
|
||||
// Get posts
|
||||
let (posts, _) = Post::fetch(&path, false).await?;
|
||||
|
||||
// Build the RSS feed
|
||||
let channel = ChannelBuilder::default()
|
||||
.title(user_str)
|
||||
.description(user_obj.description)
|
||||
.items(
|
||||
posts
|
||||
.into_iter()
|
||||
.map(|post| Item {
|
||||
title: Some(post.title.to_string()),
|
||||
link: Some(utils::get_post_url(&post)),
|
||||
author: Some(post.author.name),
|
||||
content: Some(rewrite_urls(&post.body)),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.build();
|
||||
|
||||
// Serialize the feed to RSS
|
||||
let body = channel.to_string().into_bytes();
|
||||
|
||||
// Create the HTTP response
|
||||
let mut res = Response::new(Body::from(body));
|
||||
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_fetching_user() {
|
||||
let user = user("spez").await;
|
||||
|
149
src/utils.rs
149
src/utils.rs
@ -1,20 +1,22 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::config::get_setting;
|
||||
use crate::config::{self, get_setting};
|
||||
//
|
||||
// CRATES
|
||||
//
|
||||
use crate::{client::json, server::RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rinja::Template;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde_json::Value;
|
||||
use serde_json_path::{JsonPath, JsonPathExt};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
use time::{macros::format_description, Duration, OffsetDateTime};
|
||||
use url::Url;
|
||||
|
||||
@ -327,6 +329,7 @@ pub struct Post {
|
||||
pub gallery: Vec<GalleryMedia>,
|
||||
pub awards: Awards,
|
||||
pub nsfw: bool,
|
||||
pub out_url: Option<String>,
|
||||
pub ws_url: String,
|
||||
}
|
||||
|
||||
@ -435,6 +438,7 @@ impl Post {
|
||||
awards,
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||
ws_url: val(post, "websocket_url"),
|
||||
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
|
||||
});
|
||||
}
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||
@ -788,6 +792,7 @@ pub async fn parse_post(post: &Value) -> Post {
|
||||
awards,
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||
ws_url: val(post, "websocket_url"),
|
||||
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -933,12 +938,19 @@ pub fn rewrite_urls(input_text: &str) -> String {
|
||||
// Rewrite Reddit links to Redlib
|
||||
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
|
||||
.to_string();
|
||||
text1 = REDDIT_EMOJI_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
.replace("%5C", "")
|
||||
.replace("\\_", "_");
|
||||
|
||||
loop {
|
||||
if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
|
||||
break;
|
||||
} else {
|
||||
text1 = REDDIT_EMOJI_REGEX
|
||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
text1 = text1.replace("%5C", "").replace("\\_", "_");
|
||||
|
||||
// Rewrite external media previews to Redlib
|
||||
loop {
|
||||
@ -994,6 +1006,83 @@ pub fn rewrite_urls(input_text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
|
||||
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
|
||||
|
||||
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
|
||||
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
|
||||
|
||||
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
|
||||
/* Create the paths we'll use to look for our data inside the json.
|
||||
Because we don't know the name of any given emote we use a wildcard to parse them. */
|
||||
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
|
||||
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
|
||||
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
|
||||
|
||||
// Extract all of the results from those json paths
|
||||
let link_nodes = media_metadata.json_path(&link_path);
|
||||
let id_nodes = media_metadata.json_path(&id_path);
|
||||
|
||||
// Initialize our vectors
|
||||
let mut id_vec = Vec::new();
|
||||
let mut link_vec = Vec::new();
|
||||
|
||||
// Add the relevant data to each of our vectors so we can access it by number later
|
||||
for current_id in id_nodes {
|
||||
id_vec.push(current_id)
|
||||
}
|
||||
for current_link in link_nodes {
|
||||
link_vec.push(current_link)
|
||||
}
|
||||
|
||||
/* Set index to the length of link_vec.
|
||||
This is one larger than we'll actually be looking at, but we correct that later */
|
||||
let mut index = link_vec.len();
|
||||
|
||||
// Comment needs to be in scope for when we call rewrite_urls()
|
||||
let mut comment = comment;
|
||||
|
||||
/* Loop until index hits zero.
|
||||
This also prevents us from trying to do anything on an empty vector */
|
||||
while index != 0 {
|
||||
/* Subtract 1 from index to get the real index we should be looking at.
|
||||
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
|
||||
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
|
||||
index -= 1;
|
||||
|
||||
// Convert our current index in id_vec into a string so we can search through it with regex
|
||||
let current_id = id_vec[index].to_string();
|
||||
|
||||
/* The ID number can be multiple lengths, so we capture it with regex.
|
||||
We also want to only attempt anything when we get matches to avoid panicking */
|
||||
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(¤t_id) {
|
||||
// Format the ID to include the colons it has in the comment text
|
||||
let id = format!(":{}:", &id_capture[1]);
|
||||
|
||||
// Convert current link to string to search through it with the regex
|
||||
let link = link_vec[index].to_string();
|
||||
|
||||
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
|
||||
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
|
||||
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
|
||||
It's a good idea and makes everything look nicer, so we'll do the same. */
|
||||
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
|
||||
|
||||
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
|
||||
let to_replace_with = format!(
|
||||
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
|
||||
&link_capture[1]
|
||||
);
|
||||
|
||||
// Inside the comment replace the ID we found with the string that will embed the image
|
||||
comment = comment.replace(&id, &to_replace_with).to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Call rewrite_urls() to transform any other Reddit links
|
||||
rewrite_urls(&comment)
|
||||
}
|
||||
|
||||
// Format vote count to a string that will be displayed.
|
||||
// Append `m` and `k` for millions and thousands respectively, and
|
||||
// round to the nearest tenth.
|
||||
@ -1100,6 +1189,28 @@ pub fn sfw_only() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
|
||||
/// If this variable is set as such, the instance will enable RSS feeds.
|
||||
/// Otherwise, the instance will not provide RSS feeds.
|
||||
pub fn enable_rss() -> bool {
|
||||
match get_setting("REDLIB_ENABLE_RSS") {
|
||||
Some(val) => val == "on",
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
|
||||
/// value `on`.
|
||||
///
|
||||
/// If this variable is set as such, the instance will block all robots in robots.txt and
|
||||
/// insert the noindex, nofollow meta tag on every page.
|
||||
pub fn disable_indexing() -> bool {
|
||||
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
|
||||
Some(val) => val == "on",
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Determines if a request shoud redirect to a nsfw landing gate.
|
||||
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
|
||||
let sfw_instance = sfw_only();
|
||||
@ -1155,6 +1266,20 @@ pub fn url_path_basename(path: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the URL of a post, as needed by RSS feeds
|
||||
pub fn get_post_url(post: &Post) -> String {
|
||||
if let Some(out_url) = &post.out_url {
|
||||
// Handle cross post
|
||||
if out_url.starts_with("/r/") {
|
||||
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
|
||||
} else {
|
||||
out_url.to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{format_num, format_url, rewrite_urls};
|
||||
@ -1279,3 +1404,11 @@ fn test_url_path_basename() {
|
||||
// empty path
|
||||
assert_eq!(url_path_basename("/"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewriting_emotes() {
|
||||
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
|
||||
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
|
||||
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
|
||||
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
|
||||
}
|
||||
|
2307
static/style.css
2307
static/style.css
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
/* Black theme setting */
|
||||
.black {
|
||||
--accent: #009a9a;
|
||||
--accent: #bb2b3b;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* Dark theme setting */
|
||||
.dark{
|
||||
--accent: aqua;
|
||||
--accent: #d54455;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
|
14
static/themes/libredditBlack.css
Normal file
14
static/themes/libredditBlack.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Libreddit black theme setting */
|
||||
.libredditBlack {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
--background: black;
|
||||
--outside: black;
|
||||
--post: black;
|
||||
--panel-border: 2px solid #0f0f0f;
|
||||
--highlighted: #0f0f0f;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/libredditDark.css
Normal file
14
static/themes/libredditDark.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Libreddit dark theme setting */
|
||||
.libredditDark{
|
||||
--accent: aqua;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #0f0f0f;
|
||||
--outside: #1f1f1f;
|
||||
--post: #161616;
|
||||
--panel-border: 1px solid #333;
|
||||
--highlighted: #333;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
19
static/themes/libredditLight.css
Normal file
19
static/themes/libredditLight.css
Normal file
@ -0,0 +1,19 @@
|
||||
/* Libreddit light theme setting */
|
||||
.libredditLight {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
--background: #ddd;
|
||||
--outside: #ececec;
|
||||
--post: #eee;
|
||||
--panel-border: 1px solid #ccc;
|
||||
--highlighted: white;
|
||||
--visited: #555;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html:has(> .libredditLight) {
|
||||
/* Hint color theme to browser for scrollbar */
|
||||
color-scheme: light;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/* Light theme setting */
|
||||
.light {
|
||||
--accent: #009a9a;
|
||||
--accent: #bb2b3b;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
|
@ -8,6 +8,9 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="description" content="View on Redlib, an alternative private front-end to Reddit.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% if crate::utils::disable_indexing() %}
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
{% endif %}
|
||||
<!-- General PWA -->
|
||||
<meta name="theme-color" content="#1F1F1F">
|
||||
<!-- iOS Application -->
|
||||
@ -35,7 +38,7 @@
|
||||
<nav class="
|
||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||
<div id="logo">
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a>
|
||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib.</span></a>
|
||||
{% block subscriptions %}{% endblock %}
|
||||
</div>
|
||||
{% block search %}{% endblock %}
|
||||
|
@ -24,7 +24,7 @@
|
||||
{% if author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post_link }}{{ id }}/?context=3" class="created" title="{{ created }}">{{ rel_time }}</a>
|
||||
<a href="{{ post_link }}{{ id }}/?context=3#{{ id }}" class="created" title="{{ created }}">{{ rel_time }}</a>
|
||||
{% if edited.0 != "".to_string() %}<span class="edited" title="{{ edited.1 }}">edited {{ edited.0 }}</span>{% endif %}
|
||||
{% if !awards.is_empty() && prefs.hide_awards != "on" %}
|
||||
<span class="dot">•</span>
|
||||
|
@ -10,25 +10,34 @@
|
||||
{% block content %}
|
||||
<div id="column_one">
|
||||
<form id="search_sort">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
||||
{% if sub != "" %}
|
||||
<div id="inside">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label>
|
||||
<div class="search_widget_divider_box">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
||||
<div class="search_widget_divider_box">
|
||||
{% if sub != "" %}
|
||||
<div id="inside">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %}
|
||||
<select id="sort_options" name="sort" title="Sort results by">
|
||||
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||
</select>
|
||||
{% if params.sort != "new" %}
|
||||
<select id="timeframe" name="t" title="Timeframe">
|
||||
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %}
|
||||
<select id="sort_options" name="sort" title="Sort results by">
|
||||
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||
</select>{% if params.sort != "new" %}<select id="timeframe" name="t" title="Timeframe">
|
||||
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if !is_filtered %}
|
||||
|
@ -3,6 +3,10 @@
|
||||
|
||||
{% block title %}Redlib Settings{% endblock %}
|
||||
|
||||
{% block subscriptions %}
|
||||
{% call utils::sub_list("") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search("".to_owned(), "") %}
|
||||
{% endblock %}
|
||||
@ -46,6 +50,21 @@
|
||||
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
|
||||
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="fixed_navbar">Keep navbar fixed</label>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
|
||||
<input type="hidden" value="off" name="hide_sidebar_and_summary">
|
||||
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
|
||||
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
|
||||
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Content</legend>
|
||||
@ -78,21 +97,24 @@
|
||||
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="prefs-group">
|
||||
<label for="hide_awards">Hide awards</label>
|
||||
<input type="hidden" value="off" name="hide_awards">
|
||||
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_score">Hide score</label>
|
||||
<input type="hidden" value="off" name="hide_score">
|
||||
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Media</legend>
|
||||
<div class="prefs-group">
|
||||
<label for="autoplay_videos">Autoplay videos</label>
|
||||
<input type="hidden" value="off" name="autoplay_videos">
|
||||
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="fixed_navbar">Keep navbar fixed</label>
|
||||
<input type="hidden" value="off" name="fixed_navbar">
|
||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
|
||||
<input type="hidden" value="off" name="hide_sidebar_and_summary">
|
||||
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="use_hls">Use HLS for videos</label>
|
||||
{% if prefs.ffmpeg_video_downloads != "on" %}
|
||||
@ -119,21 +141,6 @@
|
||||
<input type="hidden" value="off" name="hide_hls_notification">
|
||||
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_awards">Hide awards</label>
|
||||
<input type="hidden" value="off" name="hide_awards">
|
||||
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="hide_score">Hide score</label>
|
||||
<input type="hidden" value="off" name="hide_score">
|
||||
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
|
||||
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
|
||||
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
</fieldset>
|
||||
<input id="save" type="submit" value="Save">
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@
|
||||
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
|
||||
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
||||
</select>
|
||||
<button id="sort_submit" class="submit">
|
||||
@ -134,7 +134,13 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if crate::utils::enable_rss() %}
|
||||
<div id="sub_rss">
|
||||
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
|
||||
<button class="subscribe">RSS feed</button >
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<details class="panel" id="sidebar" open>
|
||||
|
@ -1,78 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
{% extends "base.html" %} {% import "utils.html" as utils %} {% block search %}
|
||||
{% call utils::search("".to_owned(), "") %} {% endblock %} {% block title %}{{
|
||||
user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} {%
|
||||
block subscriptions %} {% call utils::sub_list("") %} {% endblock %} {% block
|
||||
body %}
|
||||
<main>
|
||||
{% if !is_filtered %}
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<div id="listing_options">
|
||||
{% call utils::sort(["/user/", user.name.as_str()].concat(),
|
||||
["overview", "comments", "submitted"], listing) %}
|
||||
</div>
|
||||
<select id="sort_select" name="sort">
|
||||
{% call utils::options(sort.0, ["hot", "new", "top",
|
||||
"controversial"], "") %}</select
|
||||
>{% if sort.0 == "top" || sort.0 == "controversial" %}<select
|
||||
id="timeframe"
|
||||
name="t"
|
||||
>
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month",
|
||||
"year", "all"], "all") %}</select
|
||||
>{% endif %}<button id="sort_submit" class="submit">
|
||||
<svg
|
||||
width="15"
|
||||
viewBox="0 0 110 100"
|
||||
fill="none"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search("".to_owned(), "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %}
|
||||
|
||||
{% block subscriptions %}
|
||||
{% call utils::sub_list("") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
{% if !is_filtered %}
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<div id="listing_options">
|
||||
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
|
||||
</div>
|
||||
<select id="sort_select" name="sort">
|
||||
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %}
|
||||
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
<path d="M20 50 H100" />
|
||||
<path d="M75 15 L100 50 L75 85" />
|
||||
→
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if all_posts_hidden_nsfw %}
|
||||
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
|
||||
{% endif %}
|
||||
|
||||
{% if no_posts %}
|
||||
<center>No posts were found.</center>
|
||||
{% endif %}
|
||||
|
||||
{% if all_posts_filtered %}
|
||||
<center>(All content on this page has been filtered)</center>
|
||||
{% else %}
|
||||
<div id="posts">
|
||||
{% for post in posts %}
|
||||
|
||||
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
||||
{% else if !post.title.is_empty() %}
|
||||
{% call utils::post_in_list(post) %}
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score" title="{{ post.score.1 }}">
|
||||
{% if prefs.hide_score != "on" %}
|
||||
{{ post.score.0 }}
|
||||
{% else %}
|
||||
•
|
||||
{% endif %}
|
||||
{% if all_posts_hidden_nsfw %}
|
||||
<center>
|
||||
All posts are hidden because they are NSFW. Enable "Show NSFW posts"
|
||||
in settings to view.
|
||||
</center>
|
||||
{% endif %} {% if no_posts %}
|
||||
<center>No posts were found.</center>
|
||||
{% endif %} {% if all_posts_filtered %}
|
||||
<center>(All content on this page has been filtered)</center>
|
||||
{% else %}
|
||||
<div id="posts">
|
||||
{% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
|
||||
"on" %} {% else if !post.title.is_empty() %} {% call
|
||||
utils::post_in_list(post) %} {% else %}
|
||||
<div class="comment user-comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score" title="{{ post.score.1 }}">
|
||||
{% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
|
||||
else %} • {% endif %}
|
||||
</p>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a>
|
||||
<span class="created"> in </span>
|
||||
<a href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="created" title="{{ post.created }}"> {{ post.rel_time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body|safe }}</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data">
|
||||
<a
|
||||
class="comment_link"
|
||||
href="{{ post.permalink }}#{{ post.id }}"
|
||||
title="{{ post.link_title }}"
|
||||
>{{ post.link_title }}</a
|
||||
>
|
||||
<div class="user_comment_data_divider">
|
||||
<span class="created-in"> in </span>
|
||||
<a
|
||||
class="comment_subreddit"
|
||||
href="/r/{{ post.community }}"
|
||||
>r/{{ post.community }}</a
|
||||
>
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}"
|
||||
> {{ post.rel_time }}</span
|
||||
>
|
||||
</div>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body|safe }}</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %} {% endfor %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
@ -81,61 +90,94 @@
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% endif %}
|
||||
<aside>
|
||||
{% if is_filtered %}
|
||||
<center>(Content from u/{{ user.name }} has been filtered)</center>
|
||||
{% endif %}
|
||||
<div class="panel" id="user">
|
||||
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
||||
<h1 id="user_title">{{ user.title }}</h1>
|
||||
<p id="user_name">u/{{ user.name }}</p>
|
||||
<div id="user_description">{{ user.description }}</div>
|
||||
<div id="user_details">
|
||||
<label>Karma</label>
|
||||
<label>Created</label>
|
||||
<div>{{ user.karma }}</div>
|
||||
<div>{{ user.created }}</div>
|
||||
</div>
|
||||
<div id="user_actions">
|
||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||
<div id="user_subscription">
|
||||
{% if prefs.subscriptions.contains(name) %}
|
||||
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unsubscribe">Unfollow</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="subscribe">Follow</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="user_filter">
|
||||
{% if prefs.filters.contains(name) %}
|
||||
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unfilter">Unfilter</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="filter">Filter</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a
|
||||
href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}"
|
||||
accesskey="P"
|
||||
>PREV</a
|
||||
>
|
||||
{% endif %} {% if ends.1 != "" %}
|
||||
<a
|
||||
href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
|
||||
accesskey="N"
|
||||
>NEXT</a
|
||||
>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% endif %}
|
||||
<aside>
|
||||
{% if is_filtered %}
|
||||
<center>(Content from u/{{ user.name }} has been filtered)</center>
|
||||
{% endif %}
|
||||
<div class="panel" id="user">
|
||||
<img
|
||||
loading="lazy"
|
||||
id="user_icon"
|
||||
src="{{ user.icon }}"
|
||||
alt="User icon"
|
||||
/>
|
||||
<h1 id="user_title">{{ user.title }}</h1>
|
||||
<p id="user_name">u/{{ user.name }}</p>
|
||||
<div id="user_description">{{ user.description }}</div>
|
||||
<div id="user_details">
|
||||
<label>Karma</label>
|
||||
<label>Created</label>
|
||||
<div>{{ user.karma }}</div>
|
||||
<div>{{ user.created }}</div>
|
||||
</div>
|
||||
<div id="user_actions">
|
||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||
<div id="user_subscription">
|
||||
{% if prefs.subscriptions.contains(name) %}
|
||||
<form
|
||||
action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
|
||||
method="POST"
|
||||
>
|
||||
<button class="unsubscribe">Unfollow</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
|
||||
method="POST"
|
||||
>
|
||||
<button class="subscribe">Follow</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="user_filter">
|
||||
{% if prefs.filters.contains(name) %}
|
||||
<form
|
||||
action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}"
|
||||
method="POST"
|
||||
>
|
||||
<button class="unfilter">Unfilter</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
action="/r/{{ name }}/filter?redirect={{ redirect_url }}"
|
||||
method="POST"
|
||||
>
|
||||
<button class="filter">Filter</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if crate::utils::enable_rss() %}
|
||||
<div id="user_rss">
|
||||
<a
|
||||
href="/u/{{ user.name }}.rss"
|
||||
title="RSS feed for u/{{ user.name }}"
|
||||
>
|
||||
<button class="subscribe">RSS feed</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
@ -64,7 +64,7 @@
|
||||
{% macro post(post) -%}
|
||||
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
|
||||
<!-- POST CONTENT -->
|
||||
<div class="post highlighted">
|
||||
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
@ -87,12 +87,12 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
<h1 class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||
class="post_flair"
|
||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
{{ post.title }}
|
||||
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
|
||||
</h1>
|
||||
@ -104,12 +104,11 @@
|
||||
<a href="{{ post.media.url }}" class="post_media_image" >
|
||||
{% if post.media.height == 0 || post.media.width == 0 %}
|
||||
<!-- i.redd.it images special case -->
|
||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"{%if post_should_be_blurred %} class="post_nsfw_blur"{% endif %}/>
|
||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
width="{{ post.media.width }}px"
|
||||
height="{{ post.media.height }}px"
|
||||
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="100%" height="100%" href="{{ post.media.url }}"/>
|
||||
<desc>
|
||||
@ -127,7 +126,7 @@
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" {% 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>
|
||||
<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 }}" preload="none" controls>
|
||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||
</video>
|
||||
@ -135,7 +134,7 @@
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% else %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
||||
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% call render_hls_notification(post.permalink[1..]) %}
|
||||
{% endif %}
|
||||
@ -158,7 +157,10 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- POST BODY -->
|
||||
<div class="post_body">{{ post.body|safe }}</div>
|
||||
<div class="post_body">
|
||||
{{ post.body|safe }}
|
||||
{% call poll(post) %}
|
||||
</div>
|
||||
<div class="post_score" title="{{ post.score.1 }}">
|
||||
{% if prefs.hide_score != "on" %}
|
||||
{{ post.score.0 }}
|
||||
@ -180,6 +182,10 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if post.post_type == "link" %}
|
||||
<li class="desktop_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive.is</a></li>
|
||||
<li class="mobile_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive</a></li>
|
||||
{% endif %}
|
||||
{% call external_reddit_link(post.permalink) %}
|
||||
|
||||
{% if post.media.download_name != "" %}
|
||||
@ -215,7 +221,7 @@
|
||||
|
||||
{% macro post_in_list(post) -%}
|
||||
{% 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 %}" id="{{ post.id }}">
|
||||
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
|
||||
<p class="post_header">
|
||||
{% let community -%}
|
||||
{% if post.community.starts_with("u_") -%}
|
||||
@ -254,7 +260,6 @@
|
||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
|
||||
width="{{ post.media.width }}px"
|
||||
height="{{ post.media.height }}px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@ -269,19 +274,19 @@
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || post.post_type == "video") %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
<video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
|
||||
{% endif %}
|
||||
{% else if post.post_type != "self" %}
|
||||
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||
<a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||
{% if post.thumbnail.url.is_empty() %}
|
||||
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Thumbnail</title>
|
||||
@ -289,7 +294,7 @@
|
||||
</svg>
|
||||
{% else %}
|
||||
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
|
||||
<svg {% if post_should_be_blurred %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
|
||||
<desc>
|
||||
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
|
||||
|
Reference in New Issue
Block a user