Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
f9b1a832c4 | |||
1ffbce1ad8 | |||
|
793047f63f | ||
|
3625fdfdbe | ||
|
28f85f2599 | ||
|
7be29f609c | ||
|
f18d135045 | ||
f2c410f9ea | |||
|
8ef45456d6 | ||
|
118708400a | ||
|
28e72c9058 | ||
|
0b15250cc8 | ||
|
408ebe6ef1 | ||
|
7a0ea1fbd3 | ||
|
db8b92ea55 | ||
|
c494fbec31 | ||
|
438e412be3 | ||
|
d0e081e6a0 | ||
b076b076e3 | |||
fdaba8344d | |||
|
041ecceeaf | ||
|
3677ca10e2 | ||
|
21fd34710c | ||
|
4205959ade | ||
|
27b56c1781 | ||
|
374238abc3 | ||
|
9bdb5c8966 | ||
|
410872d988 | ||
|
c890e809b7 | ||
eedbe06af6 | |||
6042bca612 | |||
7e85ae25b1 | |||
|
4f21388643 | ||
|
8a917fcde3 | ||
|
67a890cab3 | ||
|
366bc17f97 | ||
|
d9e7681004 | ||
|
f74d1affb6 | ||
|
f44638a2cb | ||
|
beb4cf193b | ||
|
c565ebfb01 | ||
2b8112a3fb | |||
|
459a8e1245 | ||
|
0f7eba717e | ||
|
ea87ec33a1 | ||
|
102cd2f23f | ||
|
3b2ad212d5 | ||
|
4dc7ff8165 | ||
|
2f8a38d8c7 | ||
|
13083e999c | ||
|
4e2ec3fbc9 | ||
|
89313f73e6 | ||
1644e68e43 | |||
f5d4cc49fb | |||
f22f7841a8 | |||
b6c6e64cfe | |||
6d3db31b11 | |||
f0eb496d6a | |||
e984646ae6 | |||
60c0d63583 | |||
|
3bd8b511a7 | ||
|
8c5aaaa33d | ||
bd0a312487 | |||
|
023cc8505b | ||
|
2e476dea63 | ||
f398d16c22 | |||
|
d045a5760a | ||
|
07bf20dbc0 | ||
|
518bf03e04 | ||
|
00dee52320 | ||
|
48873c01b9 | ||
|
951fe400ae | ||
|
bacc9e35df | ||
|
724b960112 | ||
|
69f9d9ff3c | ||
|
1d44bd180e | ||
|
3301da1ef1 | ||
138934f30c | |||
5396c0783e | |||
|
213481ef53 | ||
|
2d5cfcb41d | ||
|
f460895cc0 | ||
|
5193164719 | ||
|
9013e589dd | ||
|
997cd8f829 | ||
|
5a13b9892b | ||
|
91975865b8 | ||
|
3491e754ac | ||
|
1408c32a4d | ||
|
30944579d7 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
target
|
@ -26,18 +26,24 @@ REDLIB_DEFAULT_WIDE=off
|
||||
REDLIB_DEFAULT_POST_SORT=hot
|
||||
# Set the default comment sort method (options: confidence, top, new, controversial, old)
|
||||
REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||
# Enable blurring Spoiler content by default
|
||||
REDLIB_DEFAULT_BLUR_SPOILER=off
|
||||
# Enable showing NSFW content by default
|
||||
REDLIB_DEFAULT_SHOW_NSFW=off
|
||||
# Enable blurring NSFW content by default
|
||||
REDLIB_DEFAULT_BLUR_NSFW=off
|
||||
# Enable HLS video format by default
|
||||
REDLIB_DEFAULT_USE_HLS=off
|
||||
# Enable audio+video downloads with ffmpeg.wasm
|
||||
REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
||||
# Hide HLS notification by default
|
||||
REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||
# Disable autoplay videos by default
|
||||
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
|
||||
REDLIB_DEFAULT_SUBSCRIPTIONS=
|
||||
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
|
||||
REDLIB_DEFAULT_FILTERS=
|
||||
# Hide awards by default
|
||||
REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||
# Hide sidebar and summary
|
||||
|
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
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
/target
|
||||
.env
|
||||
redlib.toml
|
||||
|
||||
# Idea Files
|
||||
.idea/
|
||||
|
||||
|
2
.replit
2
.replit
@ -1,2 +0,0 @@
|
||||
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-musl/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
|
||||
language = "bash"
|
419
Cargo.lock
generated
419
Cargo.lock
generated
@ -71,6 +71,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
@ -92,7 +98,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -118,7 +124,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"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]]
|
||||
@ -129,9 +148,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.72"
|
||||
version = "0.3.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
|
||||
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@ -142,6 +161,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"
|
||||
@ -150,9 +175,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@ -208,9 +233,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cached"
|
||||
version = "0.51.3"
|
||||
version = "0.51.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd93a9f06ec296ca66b4c26fafa9ed63f32c473d7a708a5f28563ee64c948515"
|
||||
checksum = "0feb64151eed3da6107fddd2d717a6ca4b9dbd74e43784c55c841d1abfe5a295"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"async-trait",
|
||||
@ -230,10 +255,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 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -244,9 +269,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.98"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
||||
checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -255,19 +280,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
@ -275,9 +309,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
@ -342,14 +376,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]]
|
||||
@ -362,8 +420,19 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.66",
|
||||
"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]]
|
||||
@ -372,9 +441,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 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -392,6 +461,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"
|
||||
@ -402,12 +533,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"
|
||||
@ -637,9 +786,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
version = "1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@ -655,9 +804,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
version = "0.14.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@ -679,9 +828,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",
|
||||
@ -689,7 +838,6 @@ dependencies = [
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
@ -810,9 +958,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@ -838,9 +986,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
@ -856,6 +1004,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"
|
||||
@ -872,6 +1026,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"
|
||||
@ -893,9 +1056,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.35.0"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
|
||||
checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -983,9 +1146,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.84"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -996,6 +1159,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"
|
||||
@ -1041,10 +1224,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redsunlib"
|
||||
version = "0.35.0"
|
||||
version = "0.35.2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"askama",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
"build_html",
|
||||
"cached",
|
||||
@ -1063,6 +1247,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"regex",
|
||||
"route-recognizer",
|
||||
"rss",
|
||||
"rust-embed",
|
||||
"sealed_test",
|
||||
"serde",
|
||||
@ -1077,9 +1262,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -1089,9 +1274,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -1100,9 +1285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@ -1131,6 +1316,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"
|
||||
@ -1151,7 +1348,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.68",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -1187,55 +1384,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",
|
||||
]
|
||||
|
||||
@ -1282,10 +1468,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sealed_test"
|
||||
version = "1.0.0"
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sealed_test"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1867f8f005bd7fb73c367e2e45dd628417906a2ca27597fe59cbf04279a222"
|
||||
dependencies = [
|
||||
"fs_extra",
|
||||
"rusty-forkfork",
|
||||
@ -1295,12 +1491,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sealed_test_derive"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697"
|
||||
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1343,14 +1539,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1430,18 +1626,18 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@ -1455,9 +1651,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
version = "2.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1502,7 +1698,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1540,9 +1736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
@ -1555,9 +1751,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -1574,23 +1770,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"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",
|
||||
]
|
||||
|
||||
@ -1609,9 +1804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -1630,9 +1825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -1722,9 +1917,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
@ -1733,9 +1928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
@ -1930,9 +2125,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.9"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
|
||||
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -1954,11 +2149,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
11
Cargo.toml
11
Cargo.toml
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "redsunlib"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://git.stardust.wtf/iridium/redlib"
|
||||
version = "0.35.0"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://git.stardust.wtf/iridium/redsunlib"
|
||||
version = "0.35.2"
|
||||
authors = [
|
||||
"Matthew Esposito <matt+cargo@matthew.science>",
|
||||
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
||||
@ -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"
|
||||
@ -42,6 +42,9 @@ fastrand = "2.0.1"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5.0"
|
||||
dotenvy = "0.15.7"
|
||||
rss = "2.0.7"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
lipsum = "0.9.0"
|
||||
|
130
README.md
130
README.md
@ -1,37 +1,45 @@
|
||||
<img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo">
|
||||
|
||||
# Redsunlib
|
||||
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some function and cosmetic changes.
|
||||
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some <sup><sub>(minor)</sub></sup> function and cosmetic changes.
|
||||
|
||||
<br>
|
||||
|
||||
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
|
||||
|
||||
## Table of Contents
|
||||
### Disclaimer
|
||||
|
||||
1. [Redlib](#redlib)
|
||||
2. [Instances](#instances)
|
||||
3. [About](#about)
|
||||
There are rapid changes/features in this fork that can<sup>(will)</sup> change without notice. If you want to host this version, be aware that it's likely to break at some point. I still wouldn't recommend it in a production environment unless you know what you're doing. Or like living on the edge.......
|
||||
|
||||
> I would also like to thank the maintainers and contributors of both [Redlib](https://github.com/redlib-org/redlib) and [Libreddit](https://github.com/libreddit/libreddit) for all the work they did while I just added some low quality tacky features. ❤️
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Redsunlib](#redsunlib)
|
||||
- [Disclaimer](#disclaimer)
|
||||
2. [Table of Contents](#table-of-contents)
|
||||
3. [Instances](#instances)
|
||||
4. [About](#about)
|
||||
- [The Name](#the-name)
|
||||
- [Built with](#built-with)
|
||||
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
|
||||
- [Teddit](#teddit)
|
||||
- [Libreddit](#libreddit)
|
||||
4. [Comparison](#comparison)
|
||||
5. [Comparison](#comparison)
|
||||
- [Speed](#speed)
|
||||
- [Privacy](#privacy)
|
||||
- [Reddit](#reddit)
|
||||
- [Redlib](#redlib-1)
|
||||
- [Server](#server)
|
||||
5. [Deployment](#deployment)
|
||||
6. [Deployment](#deployment)
|
||||
- [Docker](#docker)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Docker CLI](#docker-cli)
|
||||
- [Binary](#binary)
|
||||
- [Running as a systemd service](#running-as-a-systemd-service)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Replit/Heroku/Glitch](#replit-heroku-glitch)
|
||||
- [launchd (macOS)](#launchd-macos)
|
||||
6. [Configuration](#configuration)
|
||||
7. [Configuration](#configuration)
|
||||
- [Instance settings](#instance-settings)
|
||||
- [Default user settings](#default-user-settings)
|
||||
|
||||
@ -39,14 +47,10 @@
|
||||
|
||||
# Instances
|
||||
|
||||
> [!TIP]
|
||||
> 🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
|
||||
> [!WARNING]
|
||||
> 🔗 **Currently public Redsunlib instance are not available, consider using a [redlib](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) instance if you are not comfortable running your own**
|
||||
|
||||
An up-to-date table of instances is available in [Markdown](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) and [machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json).
|
||||
|
||||
Both files are part of the [redlib-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [redlib-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
|
||||
|
||||
For information on instance uptime, see the [Uptime Robot status page](https://stats.uptimerobot.com/mpmqAs1G2Q).
|
||||
You are more than welcome to host an instance and submit an issue if you want it added. That is, if you've read the [Disclaimer](#disclaimer) and it's within your "personal risk tolerance." ;)
|
||||
|
||||
---
|
||||
|
||||
@ -56,6 +60,13 @@ Redlib hopes to provide an easier way to browse Reddit, without the ads, tracker
|
||||
|
||||
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
|
||||
|
||||
## The Name
|
||||
|
||||
**Red sun** in the sky + Red**lib** = Redsunlib
|
||||
|
||||
And at the time, I was reading an excerpt from Mao Zedong, so the name seemed appropriate. But paradoxically named since Reddit is basically the sinophobia capital of the internet :/
|
||||
|
||||
|
||||
## Built with
|
||||
|
||||
- [Rust](https://www.rust-lang.org/) - Programming language
|
||||
@ -158,7 +169,7 @@ For configuration options, see the [Configuration section](#Configuration).
|
||||
|
||||
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
|
||||
|
||||
Docker images for Redlib are available at [quay.io](https://quay.io/repository/redlib/redlib), with support for `amd64`, `arm64`, and `armv7` platforms.
|
||||
Docker images for Redsunlib are available at our [Gitea container registry](https://git.stardust.wtf/iridium/-/packages/container/redsunlib/latest), currently only with support for `amd64`, if you need `arm64`, or `armv7` platforms you can either build Redsunlib yourself or open an [issue](https://git.stardust.wtf/iridium/redsunlib/issues) :)
|
||||
|
||||
### Docker Compose
|
||||
|
||||
@ -184,15 +195,15 @@ docker logs -f redlib
|
||||
Deploy Redlib:
|
||||
|
||||
```bash
|
||||
docker pull quay.io/redlib/redlib:latest
|
||||
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib:latest
|
||||
docker pull git.stardust.wtf/iridium/redsunlib:latest
|
||||
docker run -d --name redlib -p 8080:8080 git.stardust.wtf/iridium/redsunlib:latest
|
||||
```
|
||||
|
||||
Deploy using a different port on the host (in this case, port 80):
|
||||
|
||||
```bash
|
||||
docker pull quay.io/redlib/redlib:latest
|
||||
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib:latest
|
||||
docker pull git.stardust.wtf/iridium/redsunlib:latest
|
||||
docker run -d --name redlib -p 80:8080 git.stardust.wtf/iridium/redsunlib:latest
|
||||
```
|
||||
|
||||
If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`.
|
||||
@ -205,19 +216,7 @@ docker logs -f redlib
|
||||
|
||||
## Binary
|
||||
|
||||
If you're on Linux, you can grab a binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest) from GitHub.
|
||||
|
||||
Download the binary using [Wget](https://www.gnu.org/software/wget/):
|
||||
|
||||
```bash
|
||||
wget https://github.com/redlib-org/redlib/releases/download/v0.31.0/redlib
|
||||
```
|
||||
|
||||
Make the binary executable and change its ownership to `root`:
|
||||
|
||||
```bash
|
||||
sudo chmod +x redlib && sudo chown root:root redlib
|
||||
```
|
||||
Currently binaries are not supplied at this moment but will be at some point in the future but can be [built from source](#building-from-source)
|
||||
|
||||
Copy the binary to `/usr/bin`:
|
||||
|
||||
@ -261,59 +260,13 @@ Before=nginx.service
|
||||
|
||||
## Building from source
|
||||
|
||||
To deploy Redlib with changes not yet included in the latest release, you can build the application from source.
|
||||
To deploy Redsunlib with changes not yet included in the latest release, you can build the application from source.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/redlib-org/redlib && cd redlib
|
||||
git clone https://git.stardust.wtf/iridium/redsunlib && cd redsunlib
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Replit/Heroku
|
||||
|
||||
> [!WARNING]
|
||||
> These are free hosting options, but they are _not_ private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
|
||||
|
||||
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
|
||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
|
||||
|
||||
## launchd (macOS)
|
||||
|
||||
If you are on macOS, you can use the [launchd](https://en.wikipedia.org/wiki/Launchd) service available in `contrib/redlib.plist`.
|
||||
|
||||
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`.
|
||||
|
||||
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
|
||||
|
||||
<!-- ## Cargo
|
||||
|
||||
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
|
||||
|
||||
```bash
|
||||
cargo install libreddit
|
||||
``` -->
|
||||
|
||||
<!-- ## AUR
|
||||
|
||||
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
|
||||
|
||||
```bash
|
||||
yay -S libreddit-git
|
||||
```
|
||||
## NetBSD/pkgsrc
|
||||
|
||||
For NetBSD users, Redlib is available from the official repositories.
|
||||
|
||||
```bash
|
||||
pkgin install libreddit
|
||||
```
|
||||
|
||||
Or, if you prefer to build from source
|
||||
|
||||
```bash
|
||||
cd /usr/pkgsrc/libreddit
|
||||
make install
|
||||
``` -->
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
@ -343,7 +296,7 @@ REDLIB_DEFAULT_USE_HLS = "on"
|
||||
> If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example:
|
||||
>
|
||||
> ```bash
|
||||
> docker run -d --name redlib -p 8080:8080 --env-file .env quay.io/redlib/redlib:latest
|
||||
> docker run -d --name redlib -p 8080:8080 --env-file .env git.stardust.wtf/iridium/redsunlib:latest
|
||||
> ```
|
||||
>
|
||||
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
|
||||
@ -359,7 +312,8 @@ 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.
|
||||
@ -367,18 +321,22 @@ Assign a default value for each user-modifiable setting by passing environment v
|
||||
| Name | Possible values | Default value |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
|
||||
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
|
||||
| `WIDE` | `["on", "off"]` | `off` |
|
||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
|
||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||
| `FFMPEG_VIDEO_DOWNLOADS` | `["on", "off"]` | `off` |
|
||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
||||
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
|
||||
| `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` |
|
||||
|
26
app.json
26
app.json
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Redlib",
|
||||
"name": "Redsunlib",
|
||||
"description": "Private front-end for Reddit",
|
||||
"buildpacks": [
|
||||
{
|
||||
@ -14,6 +14,9 @@
|
||||
"REDLIB_DEFAULT_THEME": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_MASCOT": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_FRONT_PAGE": {
|
||||
"required": false
|
||||
},
|
||||
@ -29,13 +32,19 @@
|
||||
"REDLIB_DEFAULT_POST_SORT": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_BLUR_SPOILER": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_SHOW_NSFW": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_BLUR_NSFW": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_USE_HLS": {
|
||||
"REDLIB_DEFAULT_USE_HLS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_HIDE_HLS_NOTIFICATION": {
|
||||
@ -53,17 +62,26 @@
|
||||
"REDLIB_BANNER": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
||||
"required": false
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_FILTERS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_PUSHSHIFT_FRONTEND": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_ENABLE_RSS": {
|
||||
"required": false
|
||||
},
|
||||
"REDLIB_FULL_URL": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
ADDRESS=0.0.0.0
|
||||
PORT=12345
|
||||
#REDLIB_DEFAULT_THEME=default
|
||||
#REDLIB_DEFAULT_MASCOT=none
|
||||
#REDLIB_DEFAULT_FRONT_PAGE=default
|
||||
#REDLIB_DEFAULT_LAYOUT=card
|
||||
#REDLIB_DEFAULT_WIDE=off
|
||||
#REDLIB_DEFAULT_POST_SORT=hot
|
||||
#REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||
#REDLIB_DEFAULT_BLUR_SPOILER=off
|
||||
#REDLIB_DEFAULT_SHOW_NSFW=off
|
||||
#REDLIB_DEFAULT_BLUR_NSFW=off
|
||||
#REDLIB_DEFAULT_USE_HLS=off
|
||||
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
||||
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)
|
||||
|
31
scripts/load_test.py
Normal file
31
scripts/load_test.py
Normal file
@ -0,0 +1,31 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
base_url = "http://localhost:8080"
|
||||
|
||||
full_path = f"{base_url}/r/politics"
|
||||
|
||||
ctr = 0
|
||||
|
||||
def fetch_url(url):
|
||||
global ctr
|
||||
response = requests.get(url)
|
||||
ctr += 1
|
||||
print(f"Request count: {ctr}")
|
||||
return response
|
||||
|
||||
while full_path:
|
||||
response = requests.get(full_path)
|
||||
ctr += 1
|
||||
print(f"Request count: {ctr}")
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
comment_links = soup.find_all('a', class_='post_comments')
|
||||
comment_urls = [base_url + link['href'] for link in comment_links]
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
executor.map(fetch_url, comment_urls)
|
||||
next_link = soup.find('a', accesskey='N')
|
||||
if next_link:
|
||||
full_path = base_url + next_link['href']
|
||||
else:
|
||||
break
|
129
src/client.rs
129
src/client.rs
@ -1,17 +1,21 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use cached::proc_macro::cached;
|
||||
use futures_lite::future::block_on;
|
||||
use futures_lite::{future::Boxed, FutureExt};
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::StatusCode;
|
||||
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use libflate::gzip;
|
||||
use log::error;
|
||||
use log::{error, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::{percent_encode, CONTROLS};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16};
|
||||
use std::{io, result::Result};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::dbg_msg;
|
||||
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
|
||||
@ -19,23 +23,23 @@ use crate::server::RequestExt;
|
||||
use crate::utils::format_url;
|
||||
|
||||
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
|
||||
|
||||
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.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)
|
||||
});
|
||||
|
||||
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
|
||||
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
|
||||
let client = block_on(Oauth::new());
|
||||
tokio::spawn(token_daemon());
|
||||
RwLock::new(client)
|
||||
ArcSwap::new(client.into())
|
||||
});
|
||||
|
||||
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
|
||||
|
||||
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Gets the canonical path for a resource on Reddit. This is accomplished by
|
||||
/// making a `HEAD` request to Reddit at the path given in `path`.
|
||||
///
|
||||
@ -52,10 +56,9 @@ pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
|
||||
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||
let res = reddit_head(path.clone(), true).await?;
|
||||
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)),
|
||||
|
||||
@ -86,6 +89,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()
|
||||
@ -170,8 +179,8 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
// Construct the hyper client from the HTTPS connector.
|
||||
let client: Client<_, Body> = CLIENT.clone();
|
||||
|
||||
let (token, vendor_id, device_id, mut user_agent, loid) = {
|
||||
let client = block_on(OAUTH_CLIENT.read());
|
||||
let (token, vendor_id, device_id, user_agent, loid) = {
|
||||
let client = OAUTH_CLIENT.load_full();
|
||||
(
|
||||
client.token.clone(),
|
||||
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
|
||||
@ -181,13 +190,6 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
)
|
||||
};
|
||||
|
||||
// Replace "Android" with a tricky word.
|
||||
// Issues: #78/#115, #116
|
||||
// If you include the word "Android", you will get a number of different errors
|
||||
// I guess they don't expect mobile traffic on the endpoints we use
|
||||
// Scrawled on wall for next poor soul: Run the test suite.
|
||||
user_agent = user_agent.replace("Android", "Andr\u{200B}oid");
|
||||
|
||||
// Build request to Reddit. When making a GET, request gzip compression.
|
||||
// (Reddit doesn't do brotli yet.)
|
||||
let builder = Request::builder()
|
||||
@ -222,12 +224,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
if !redirect {
|
||||
return Ok(response);
|
||||
};
|
||||
|
||||
let location_header = response.headers().get(header::LOCATION);
|
||||
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
|
||||
return Err("Reddit response was invalid".to_string());
|
||||
}
|
||||
return request(
|
||||
method,
|
||||
response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
location_header
|
||||
.map(|val| {
|
||||
// We need to make adjustments to the URI
|
||||
// we get back from Reddit. Namely, we
|
||||
@ -240,7 +243,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
// required.
|
||||
//
|
||||
// 2. Percent-encode the path.
|
||||
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string();
|
||||
let new_path = percent_encode(val.as_bytes(), CONTROLS)
|
||||
.to_string()
|
||||
.trim_start_matches(REDDIT_URL_BASE)
|
||||
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
|
||||
.to_string();
|
||||
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@ -251,6 +258,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
.await;
|
||||
};
|
||||
|
||||
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
|
||||
if response.status() == StatusCode::FORBIDDEN && response.headers().get("retry-after").unwrap_or(&HeaderValue::from_static("0")).to_str().unwrap_or("0") == "0" {
|
||||
force_refresh_token().await;
|
||||
return Err("Rate limit - try refreshing soon".to_string());
|
||||
}
|
||||
|
||||
match response.headers().get(header::CONTENT_ENCODING) {
|
||||
// Content not compressed.
|
||||
None => Ok(response),
|
||||
@ -299,7 +312,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
dbg_msg!("{} {}: {}", method, path, e);
|
||||
dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
|
||||
|
||||
Err(e.to_string())
|
||||
}
|
||||
@ -314,19 +327,56 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
||||
#[cached(size = 100, time = 30, result = true)]
|
||||
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
// Closure to quickly build errors
|
||||
let err = |msg: &str, e: String| -> Result<Value, String> {
|
||||
let err = |msg: &str, e: String, path: String| -> Result<Value, String> {
|
||||
// eprintln!("{} - {}: {}", url, msg, e);
|
||||
Err(format!("{msg}: {e}"))
|
||||
Err(format!("{msg}: {e} | {path}"))
|
||||
};
|
||||
|
||||
// First, handle rolling over the OAUTH_CLIENT if need be.
|
||||
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
|
||||
let is_rolling_over = OAUTH_IS_ROLLING_OVER.load(Ordering::SeqCst);
|
||||
if current_rate_limit < 10 && !is_rolling_over {
|
||||
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
|
||||
tokio::spawn(force_refresh_token());
|
||||
}
|
||||
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
|
||||
|
||||
// Fetch the url...
|
||||
match reddit_get(path.clone(), quarantine).await {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
|
||||
let reset: Option<String> = if let (Some(remaining), Some(reset), Some(used)) = (
|
||||
response.headers().get("x-ratelimit-remaining").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
||||
response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
||||
response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
||||
) {
|
||||
trace!(
|
||||
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
|
||||
if is_rolling_over { "yes" } else { "no" },
|
||||
);
|
||||
Some(reset)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// asynchronously aggregate the chunks of the body
|
||||
match hyper::body::aggregate(response).await {
|
||||
Ok(body) => {
|
||||
let has_remaining = body.has_remaining();
|
||||
|
||||
if !has_remaining {
|
||||
// Rate limited, so spawn a force_refresh_token()
|
||||
tokio::spawn(force_refresh_token());
|
||||
return match reset {
|
||||
Some(val) => Err(format!(
|
||||
"Reddit rate limit exceeded. Try refreshing in a few seconds.\
|
||||
Rate limit will reset in: {val}"
|
||||
)),
|
||||
None => Err("Reddit rate limit exceeded".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the response from Reddit as JSON
|
||||
match serde_json::from_reader(body.reader()) {
|
||||
Ok(value) => {
|
||||
@ -339,7 +389,15 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
let () = force_refresh_token().await;
|
||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
||||
}
|
||||
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"]))
|
||||
// Handle quarantined
|
||||
if json["reason"] == "quarantined" {
|
||||
return Err("quarantined".into());
|
||||
}
|
||||
// Handle gated
|
||||
if json["reason"] == "gated" {
|
||||
return Err("gated".into());
|
||||
}
|
||||
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
|
||||
} else {
|
||||
Ok(json)
|
||||
}
|
||||
@ -349,21 +407,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||
if status.is_server_error() {
|
||||
Err("Reddit is having issues, check if there's an outage".to_string())
|
||||
} else {
|
||||
err("Failed to parse page JSON data", e.to_string())
|
||||
err("Failed to parse page JSON data", e.to_string(), path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => err("Failed receiving body from Reddit", e.to_string()),
|
||||
Err(e) => err("Failed receiving body from Reddit", e.to_string(), path),
|
||||
}
|
||||
}
|
||||
Err(e) => err("Couldn't send request to Reddit", e),
|
||||
Err(e) => err("Couldn't send request to Reddit", e, path),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_localization_popular() {
|
||||
let val = json("/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL".to_string(), false).await.unwrap();
|
||||
let val = json(POPULAR_URL.to_string(), false).await.unwrap();
|
||||
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,10 @@ pub struct Config {
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
|
||||
pub(crate) default_post_sort: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_BLUR_SPOILER")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_BLUR_SPOILER")]
|
||||
pub(crate) default_blur_spoiler: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
|
||||
pub(crate) default_show_nsfw: Option<String>,
|
||||
@ -88,6 +92,10 @@ pub struct Config {
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
|
||||
pub(crate) default_subscriptions: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_FILTERS")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
|
||||
pub(crate) default_filters: Option<String>,
|
||||
|
||||
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
||||
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
||||
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
|
||||
@ -103,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 {
|
||||
@ -135,6 +149,7 @@ impl Config {
|
||||
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
|
||||
default_wide: parse("REDLIB_DEFAULT_WIDE"),
|
||||
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
|
||||
default_blur_spoiler: parse("REDLIB_DEFAULT_BLUR_SPOILER"),
|
||||
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
|
||||
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
|
||||
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
|
||||
@ -144,10 +159,13 @@ impl Config {
|
||||
default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
|
||||
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
|
||||
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
|
||||
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
|
||||
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
|
||||
banner: parse("REDLIB_BANNER"),
|
||||
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
|
||||
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
|
||||
enable_rss: parse("REDLIB_ENABLE_RSS"),
|
||||
full_url: parse("REDLIB_FULL_URL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,6 +179,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
|
||||
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
|
||||
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
|
||||
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
|
||||
"REDLIB_DEFAULT_BLUR_SPOILER" => config.default_blur_spoiler.clone(),
|
||||
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
|
||||
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
|
||||
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
|
||||
@ -171,10 +190,13 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
|
||||
"REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(),
|
||||
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
|
||||
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
|
||||
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
|
||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
|
||||
"REDLIB_BANNER" => config.banner.clone(),
|
||||
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
|
||||
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
|
||||
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
|
||||
"REDLIB_FULL_URL" => config.full_url.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -243,6 +265,12 @@ fn test_default_subscriptions() {
|
||||
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[sealed_test(env = [("REDLIB_DEFAULT_FILTERS", "news+bestof")])]
|
||||
fn test_default_filters() {
|
||||
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[sealed_test]
|
||||
fn test_pushshift() {
|
||||
|
@ -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"]),
|
||||
@ -142,11 +144,14 @@ impl InstanceInfo {
|
||||
["Wide", &convert(&self.config.default_wide)],
|
||||
["Comment sort", &convert(&self.config.default_comment_sort)],
|
||||
["Post sort", &convert(&self.config.default_post_sort)],
|
||||
["Blur Spoiler", &convert(&self.config.default_blur_spoiler)],
|
||||
["Show NSFW", &convert(&self.config.default_show_nsfw)],
|
||||
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
|
||||
["Use HLS", &convert(&self.config.default_use_hls)],
|
||||
["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)],
|
||||
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
|
||||
["Subscriptions", &convert(&self.config.default_subscriptions)],
|
||||
["Filters", &convert(&self.config.default_filters)],
|
||||
])
|
||||
.with_header_row(["Default preferences"]),
|
||||
);
|
||||
@ -164,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
|
||||
@ -175,11 +182,14 @@ impl InstanceInfo {
|
||||
Default wide: {:?}\n
|
||||
Default comment sort: {:?}\n
|
||||
Default post sort: {:?}\n
|
||||
Default blur Spoiler: {:?}\n
|
||||
Default show NSFW: {:?}\n
|
||||
Default blur NSFW: {:?}\n
|
||||
Default use HLS: {:?}\n
|
||||
Default use FFmpeg: {:?}\n
|
||||
Default hide HLS notification: {:?}\n
|
||||
Default subscriptions: {:?}\n",
|
||||
Default subscriptions: {:?}\n
|
||||
Default filters: {:?}\n",
|
||||
self.package_name,
|
||||
self.crate_version,
|
||||
self.git_commit,
|
||||
@ -187,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,
|
||||
@ -198,11 +210,14 @@ impl InstanceInfo {
|
||||
self.config.default_wide,
|
||||
self.config.default_comment_sort,
|
||||
self.config.default_post_sort,
|
||||
self.config.default_blur_spoiler,
|
||||
self.config.default_show_nsfw,
|
||||
self.config.default_blur_nsfw,
|
||||
self.config.default_use_hls,
|
||||
self.config.default_ffmpeg_video_downloads,
|
||||
self.config.default_hide_hls_notification,
|
||||
self.config.default_subscriptions,
|
||||
self.config.default_filters,
|
||||
)
|
||||
}
|
||||
StringType::Html => self.to_table(),
|
||||
|
@ -160,7 +160,7 @@ async fn main() {
|
||||
.long("address")
|
||||
.value_name("ADDRESS")
|
||||
.help("Sets address to listen on")
|
||||
.default_value("0.0.0.0")
|
||||
.default_value("[::]")
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
@ -299,6 +299,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 +314,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")
|
||||
|
37
src/oauth.rs
37
src/oauth.rs
@ -1,12 +1,12 @@
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
|
||||
|
||||
use crate::{
|
||||
client::{CLIENT, OAUTH_CLIENT},
|
||||
client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
|
||||
oauth_resources::ANDROID_APP_VERSION_LIST,
|
||||
};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hyper::{client, Body, Method, Request};
|
||||
use log::info;
|
||||
use log::{info, trace};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
@ -98,21 +98,13 @@ impl Oauth {
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn refresh(&mut self) -> Option<()> {
|
||||
// Refresh is actually just a subsequent login with the same headers (without the old token
|
||||
// or anything). This logic is handled in login, so we just call login again.
|
||||
let refresh = self.login().await;
|
||||
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
|
||||
refresh
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn token_daemon() {
|
||||
// Monitor for refreshing token
|
||||
loop {
|
||||
// Get expiry time - be sure to not hold the read lock
|
||||
let expires_in = { OAUTH_CLIENT.read().await.expires_in };
|
||||
let expires_in = { OAUTH_CLIENT.load_full().expires_in };
|
||||
|
||||
// sleep for the expiry time minus 2 minutes
|
||||
let duration = Duration::from_secs(expires_in - 120);
|
||||
@ -125,13 +117,22 @@ pub async fn token_daemon() {
|
||||
|
||||
// Refresh token - in its own scope
|
||||
{
|
||||
OAUTH_CLIENT.write().await.refresh().await;
|
||||
force_refresh_token().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn force_refresh_token() {
|
||||
OAUTH_CLIENT.write().await.refresh().await;
|
||||
if OAUTH_IS_ROLLING_OVER.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
|
||||
trace!("Skipping refresh token roll over, already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
|
||||
let new_client = Oauth::new().await;
|
||||
OAUTH_CLIENT.swap(new_client.into());
|
||||
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
|
||||
OAUTH_IS_ROLLING_OVER.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@ -179,21 +180,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_oauth_client() {
|
||||
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
|
||||
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_oauth_client_refresh() {
|
||||
OAUTH_CLIENT.write().await.refresh().await.unwrap();
|
||||
force_refresh_token().await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_oauth_token_exists() {
|
||||
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
|
||||
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_oauth_headers_len() {
|
||||
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3);
|
||||
assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -19,7 +19,7 @@ struct SettingsTemplate {
|
||||
|
||||
// CONSTANTS
|
||||
|
||||
const PREFS: [&str; 18] = [
|
||||
const PREFS: [&str; 19] = [
|
||||
"theme",
|
||||
"mascot",
|
||||
"front_page",
|
||||
@ -27,6 +27,7 @@ const PREFS: [&str; 18] = [
|
||||
"wide",
|
||||
"comment_sort",
|
||||
"post_sort",
|
||||
"blur_spoiler",
|
||||
"show_nsfw",
|
||||
"blur_nsfw",
|
||||
"use_hls",
|
||||
@ -122,7 +123,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
||||
Some(value) => response.insert_cookie(
|
||||
Cookie::build((name.to_owned(), value.clone()))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.http_only(name != "ffmpeg_video_downloads")
|
||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||
.into(),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
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,
|
||||
@ -119,7 +120,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
params.push_str(&format!("&geo_filter={geo_filter}"));
|
||||
}
|
||||
|
||||
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default());
|
||||
let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
|
||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
|
||||
let filters = get_filters(&req);
|
||||
@ -145,6 +146,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
||||
let no_posts = posts.is_empty();
|
||||
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
||||
if sort == "new" {
|
||||
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
|
||||
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
|
||||
}
|
||||
Ok(template(&SubredditTemplate {
|
||||
sub,
|
||||
posts,
|
||||
@ -455,8 +460,66 @@ 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)),
|
||||
..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());
|
||||
}
|
||||
|
51
src/user.rs
51
src/user.rs
@ -2,6 +2,7 @@
|
||||
use crate::client::json;
|
||||
use crate::server::RequestExt;
|
||||
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
|
||||
use crate::{config, utils};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use time::{macros::format_description, OffsetDateTime};
|
||||
@ -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;
|
||||
|
103
src/utils.rs
103
src/utils.rs
@ -1,4 +1,5 @@
|
||||
use crate::config::get_setting;
|
||||
#![allow(dead_code)]
|
||||
use crate::config::{self, get_setting};
|
||||
//
|
||||
// CRATES
|
||||
//
|
||||
@ -14,6 +15,7 @@ use serde_json::Value;
|
||||
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;
|
||||
|
||||
@ -156,6 +158,7 @@ impl PollOption {
|
||||
|
||||
// Post flags with nsfw and stickied
|
||||
pub struct Flags {
|
||||
pub spoiler: bool,
|
||||
pub nsfw: bool,
|
||||
pub stickied: bool,
|
||||
}
|
||||
@ -167,6 +170,7 @@ pub struct Media {
|
||||
pub width: i64,
|
||||
pub height: i64,
|
||||
pub poster: String,
|
||||
pub download_name: String,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
@ -233,6 +237,15 @@ impl Media {
|
||||
|
||||
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
||||
|
||||
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
|
||||
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
|
||||
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
|
||||
|
||||
format!("redlib_{permalink_base}_{media_url_base}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
(
|
||||
post_type.to_string(),
|
||||
Self {
|
||||
@ -243,6 +256,7 @@ impl Media {
|
||||
width: source["width"].as_i64().unwrap_or_default(),
|
||||
height: source["height"].as_i64().unwrap_or_default(),
|
||||
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
||||
download_name,
|
||||
},
|
||||
gallery,
|
||||
)
|
||||
@ -296,6 +310,7 @@ pub struct Post {
|
||||
pub body: String,
|
||||
pub author: Author,
|
||||
pub permalink: String,
|
||||
pub link_title: String,
|
||||
pub poll: Option<Poll>,
|
||||
pub score: (String, String),
|
||||
pub upvote_ratio: i64,
|
||||
@ -307,11 +322,13 @@ pub struct Post {
|
||||
pub domain: String,
|
||||
pub rel_time: String,
|
||||
pub created: String,
|
||||
pub created_ts: u64,
|
||||
pub num_duplicates: u64,
|
||||
pub comments: (String, String),
|
||||
pub gallery: Vec<GalleryMedia>,
|
||||
pub awards: Awards,
|
||||
pub nsfw: bool,
|
||||
pub out_url: Option<String>,
|
||||
pub ws_url: String,
|
||||
}
|
||||
|
||||
@ -338,6 +355,7 @@ impl Post {
|
||||
let data = &post["data"];
|
||||
|
||||
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
||||
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
|
||||
let score = data["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
let title = val(post, "title");
|
||||
@ -384,6 +402,7 @@ impl Post {
|
||||
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||
poster: String::new(),
|
||||
download_name: String::new(),
|
||||
},
|
||||
media,
|
||||
domain: val(post, "domain"),
|
||||
@ -402,22 +421,25 @@ impl Post {
|
||||
},
|
||||
},
|
||||
flags: Flags {
|
||||
spoiler: data["spoiler"].as_bool().unwrap_or_default(),
|
||||
nsfw: data["over_18"].as_bool().unwrap_or_default(),
|
||||
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
|
||||
},
|
||||
permalink: val(post, "permalink"),
|
||||
link_title: val(post, "link_title"),
|
||||
poll: Poll::parse(&data["poll_data"]),
|
||||
rel_time,
|
||||
created,
|
||||
created_ts,
|
||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
|
||||
gallery,
|
||||
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()))
|
||||
}
|
||||
}
|
||||
@ -576,6 +598,7 @@ pub struct Preferences {
|
||||
pub front_page: String,
|
||||
pub layout: String,
|
||||
pub wide: String,
|
||||
pub blur_spoiler: String,
|
||||
pub show_nsfw: String,
|
||||
pub blur_nsfw: String,
|
||||
pub hide_hls_notification: String,
|
||||
@ -628,6 +651,7 @@ impl Preferences {
|
||||
front_page: setting(req, "front_page"),
|
||||
layout: setting(req, "layout"),
|
||||
wide: setting(req, "wide"),
|
||||
blur_spoiler: setting(req, "blur_spoiler"),
|
||||
show_nsfw: setting(req, "show_nsfw"),
|
||||
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
|
||||
blur_nsfw: setting(req, "blur_nsfw"),
|
||||
@ -686,6 +710,8 @@ pub async fn parse_post(post: &Value) -> Post {
|
||||
// Determine the type of media along with the media URL
|
||||
let (post_type, media, gallery) = Media::parse(&post["data"]).await;
|
||||
|
||||
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
|
||||
|
||||
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
|
||||
|
||||
let permalink = val(post, "permalink");
|
||||
@ -722,6 +748,7 @@ pub async fn parse_post(post: &Value) -> Post {
|
||||
distinguished: val(post, "distinguished"),
|
||||
},
|
||||
permalink,
|
||||
link_title: val(post, "link_title"),
|
||||
poll,
|
||||
score: format_num(score),
|
||||
upvote_ratio: ratio as i64,
|
||||
@ -733,6 +760,7 @@ pub async fn parse_post(post: &Value) -> Post {
|
||||
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||
poster: String::new(),
|
||||
download_name: String::new(),
|
||||
},
|
||||
flair: Flair {
|
||||
flair_parts: FlairPart::parse(
|
||||
@ -749,18 +777,21 @@ pub async fn parse_post(post: &Value) -> Post {
|
||||
},
|
||||
},
|
||||
flags: Flags {
|
||||
spoiler: post["data"]["spoiler"].as_bool().unwrap_or_default(),
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
|
||||
},
|
||||
domain: val(post, "domain"),
|
||||
rel_time,
|
||||
created,
|
||||
created_ts,
|
||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
|
||||
gallery,
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1046,7 +1077,7 @@ pub fn redirect(path: &str) -> Response<Body> {
|
||||
|
||||
/// Renders a generic error landing page.
|
||||
pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
|
||||
error!("Error page rendered: {msg}");
|
||||
error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
|
||||
let url = req.uri().to_string();
|
||||
let body = ErrorTemplate {
|
||||
msg: msg.to_string(),
|
||||
@ -1073,6 +1104,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();
|
||||
@ -1114,6 +1167,34 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
|
||||
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
|
||||
}
|
||||
|
||||
// Returns the last (non-empty) segment of a path string
|
||||
pub fn url_path_basename(path: &str) -> String {
|
||||
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
|
||||
|
||||
if url_result.is_err() {
|
||||
path.to_string()
|
||||
} else {
|
||||
let mut url = url_result.unwrap();
|
||||
url.path_segments_mut().unwrap().pop_if_empty();
|
||||
|
||||
url.path_segments().unwrap().last().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// 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};
|
||||
@ -1222,3 +1303,19 @@ fn test_rewriting_image_links() {
|
||||
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
|
||||
assert_eq!(rewrite_urls(input), output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_path_basename() {
|
||||
// without trailing slash
|
||||
assert_eq!(url_path_basename("/first/last"), "last");
|
||||
// with trailing slash
|
||||
assert_eq!(url_path_basename("/first/last/"), "last");
|
||||
// with query parameters
|
||||
assert_eq!(url_path_basename("/first/last/?some=query"), "last");
|
||||
// file path
|
||||
assert_eq!(url_path_basename("/cdn/image.jpg"), "image.jpg");
|
||||
// when a full url is passed instead of just a path
|
||||
assert_eq!(url_path_basename("https://doma.in/first/last"), "last");
|
||||
// empty path
|
||||
assert_eq!(url_path_basename("/"), "");
|
||||
}
|
||||
|
2399
static/style.css
2399
static/style.css
File diff suppressed because it is too large
Load Diff
@ -115,7 +115,7 @@ let ffmpeg = null;
|
||||
|
||||
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
|
||||
const data = await ffmpeg.readFile('output.mp4');
|
||||
saveAs(new Blob([data.buffer]),filename);
|
||||
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
|
||||
return
|
||||
}
|
||||
function saveAs(blob, filename) { // Yeah ok...
|
||||
|
@ -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 -->
|
||||
@ -28,7 +31,7 @@
|
||||
</head>
|
||||
<body class="
|
||||
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
|
||||
{% if prefs.wide == "on" %} wide{% endif %}
|
||||
{% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %} wide{% endif %}
|
||||
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
|
||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||
<!-- NAVIGATION BAR -->
|
||||
|
@ -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>
|
||||
|
@ -2,10 +2,14 @@
|
||||
{% block title %}Error: {{ msg }}{% endblock %}
|
||||
{% block sortstyle %}{% endblock %}
|
||||
{% block content %}
|
||||
<div id="error">
|
||||
<h1>{{ msg }}</h1>
|
||||
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
||||
<br />
|
||||
<h3>Head back <a href="/">home</a>?</h3>
|
||||
</div>
|
||||
<div id="error">
|
||||
<h1>{{ msg }}</h1>
|
||||
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
||||
<br />
|
||||
<h3>Expected something to work? <a
|
||||
href="https://github.com/redlib-org/redlib/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=%F0%9F%90%9B+Bug+Report%3A+{{ msg }}">Report
|
||||
an issue</a></h3>
|
||||
<br />
|
||||
<h3>Head back <a href="/">home</a>?</h3>
|
||||
</div>
|
||||
{% endblock %}
|
@ -6,11 +6,11 @@
|
||||
<h1>
|
||||
😱
|
||||
{% if res_type == crate::utils::ResourceType::Subreddit %}
|
||||
r/{{ res }} is a NSFW community!
|
||||
r/{{ res }} is a <b class="nsfw-tag">NSFW</b> community!
|
||||
{% else if res_type == crate::utils::ResourceType::User %}
|
||||
u/{{ res }}'s content is NSFW!
|
||||
u/{{ res }}'s content is <b class="nsfw-tag">NSFW</b>!
|
||||
{% else if res_type == crate::utils::ResourceType::Post %}
|
||||
This post is NSFW!
|
||||
This post is <b class="nsfw-tag">NSFW</b>!
|
||||
{% endif %}
|
||||
</h1>
|
||||
<br />
|
||||
@ -20,6 +20,7 @@
|
||||
This instance of Redlib is SFW-only.</p>
|
||||
{% else %}
|
||||
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br>
|
||||
<div>Alternatively <a href="/settings/update/?show_nsfw=on&redirect={{self.url[1..self.url.len()]}}">enable NSFW posts</a> now and view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}profile{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %} immediately</div>
|
||||
{% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
@ -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 %}
|
||||
@ -37,13 +41,14 @@
|
||||
<div class="prefs-group">
|
||||
<label for="layout">Layout:</label>
|
||||
<select name="layout" id="layout">
|
||||
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
|
||||
{% call utils::options(prefs.layout, ["card", "clean", "compact", "old", "waterfall"], "card") %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="wide">Wide UI:</label>
|
||||
<input type="hidden" value="off" name="wide">
|
||||
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||
<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>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@ -60,6 +65,11 @@
|
||||
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="blur_spoiler">Blur spoiler previews:</label>
|
||||
<input type="hidden" value="off" name="blur_spoiler">
|
||||
<input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
{% if !crate::utils::sfw_only() %}
|
||||
<div class="prefs-group">
|
||||
<label for="show_nsfw">Show NSFW posts:</label>
|
||||
@ -132,6 +142,10 @@
|
||||
<input id="save" type="submit" value="Save">
|
||||
</div>
|
||||
</form>
|
||||
<div id="settings_note">
|
||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||
</div>
|
||||
{% if prefs.subscriptions.len() > 0 %}
|
||||
<div class="prefs" id="settings_subs">
|
||||
<legend>Subscribed Feeds</legend>
|
||||
@ -162,11 +176,6 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="settings_note">
|
||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -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,76 +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 }}">Comment on 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 }}"
|
||||
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>
|
||||
@ -79,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 %}
|
||||
|
@ -62,8 +62,9 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro post(post) -%}
|
||||
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
|
||||
<!-- POST CONTENT -->
|
||||
<div class="post highlighted">
|
||||
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
@ -86,13 +87,14 @@
|
||||
{% 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>
|
||||
|
||||
<!-- POST MEDIA -->
|
||||
@ -101,7 +103,7 @@
|
||||
<div class="post_media_content">
|
||||
<a href="{{ post.media.url }}" class="post_media_image" >
|
||||
{% if post.media.height == 0 || post.media.width == 0 %}
|
||||
<!-- i.redd.it images speical case -->
|
||||
<!-- i.redd.it images special case -->
|
||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
@ -165,13 +167,32 @@
|
||||
<span class="label"> Upvotes</span></div>
|
||||
<div class="post_footer">
|
||||
<ul id="post_links">
|
||||
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
|
||||
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li>
|
||||
<li>
|
||||
<a href="{{ post.permalink }}">
|
||||
<span class="desktop_item">perma</span>link
|
||||
</a>
|
||||
</li>
|
||||
{% if post.num_duplicates > 0 %}
|
||||
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
|
||||
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
|
||||
<li>
|
||||
<a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
|
||||
dup<span class="desktop_item">licat</span>es
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% 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 != "" %}
|
||||
<li>
|
||||
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
|
||||
<span class="mobile_item">dl</span>
|
||||
<span class="desktop_item">download</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
|
||||
</div>
|
||||
@ -179,8 +200,7 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro external_reddit_link(permalink) %}
|
||||
{% for dev_type in ["desktop", "mobile"] %}
|
||||
<li class="{{ dev_type }}_item">
|
||||
<li>
|
||||
<a
|
||||
{% if prefs.disable_visit_reddit_confirmation != "on" %}
|
||||
href="#popup"
|
||||
@ -194,11 +214,11 @@
|
||||
{% call visit_reddit_confirmation(permalink) %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro post_in_list(post) -%}
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
|
||||
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
|
||||
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
|
||||
<p class="post_header">
|
||||
{% let community -%}
|
||||
{% if post.community.starts_with("u_") -%}
|
||||
@ -226,10 +246,10 @@
|
||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
|
||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
|
||||
</h2>
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
|
||||
{% if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && post.post_type == "image" %}
|
||||
<div class="post_media_content">
|
||||
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
|
||||
{% if post.media.height == 0 || post.media.width == 0 %}
|
||||
@ -237,7 +257,6 @@
|
||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||
{% else %}
|
||||
<svg
|
||||
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
|
||||
width="{{ post.media.width }}px"
|
||||
height="{{ post.media.height }}px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@ -249,26 +268,22 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || 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.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
<video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
|
||||
{% endif %}
|
||||
{% else if post.post_type != "self" %}
|
||||
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||
<a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||
{% if post.thumbnail.url.is_empty() %}
|
||||
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Thumbnail</title>
|
||||
@ -276,7 +291,7 @@
|
||||
</svg>
|
||||
{% else %}
|
||||
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
|
||||
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
||||
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
|
||||
<desc>
|
||||
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
|
||||
|
Loading…
Reference in New Issue
Block a user