Compare commits

..

60 Commits

Author SHA1 Message Date
1c1e627815 v0.35.3 2024-10-20 19:31:07 +13:00
4b854eb84d fix/change feed list alignment 2024-10-20 19:20:01 +13:00
712790acbe fix footer spacing 2024-10-20 18:08:26 +13:00
062d810aad settings rearrangement 2024-10-20 18:06:26 +13:00
64eec64ebe Merge remote-tracking branch 'upstream/main' 2024-10-20 17:32:13 +13:00
3ff907d6c1 additional new colour tweaks (#285) 2024-10-12 11:42:15 -04:00
f4a457e529 Add additional themes to README (#284) 2024-10-11 15:43:45 -04:00
7dda8d9bbb use better accent colour + add libreddit styles (#281)
* update favicon with new logo

* only have 32x32 .ico file

* use #d74253 as new accent colour + add old libreddit styles + bolden accented buttons

* fix unrenamed libreddit themes
2024-10-10 18:45:39 -04:00
b99412b4a1 feat: update logos and accents (#280)
* feat: update logos, accents

* fix apple-touch-icon.png size

* remove width, length

---------

Co-authored-by: DokterKaj <dokterkaj@gmail.com>
2024-10-06 15:19:37 -04:00
1838fdaea4 Replace askama with rinja (#276) 2024-10-02 17:43:13 -04:00
f71b0cd178 chore(deps): Update brotli from 6.0 to 7.0 (#277)
* chore(deps): Update brotli from 6.0 to 7.0

* Update Cargo.lock for brotli 7.0
2024-10-02 14:18:41 -04:00
604db902e9 Fix systemd service (#275)
This format is not recognized by systemd. As shown in the following log:

/etc/systemd/system/redlib.service:33: System call ~@privileged is not known, ignoring.
/etc/systemd/system/redlib.service:33: System call ~@resources is not known, ignoring.
2024-10-01 15:55:42 -04:00
e57eaa0b78 fix(issue): render checkbox in issue template 2024-09-29 22:31:23 -04:00
31ad8c5f7b fix(issue): add checkbox for latest commit 2024-09-29 16:29:38 -04:00
5aef97410c Update redlib.conf (#271)
This setting expects an array of subs and not a boolean value. 
This confuses new users and also seems to be unintentional. 

Removing the comment character would lead to an error output on the start page. “Couldn't send request to Reddit: Post url contains non-ASCII characters | /r/off (sub1%2Bsub2%2Bsub3)/hot.json?&raw_json=1”
2024-09-29 14:45:03 -04:00
8d0ed4682e feat(search): redirect u/ and user/ to profile (#268) 2024-09-27 08:29:33 -04:00
fe4fed0504 Make jump to comment work from user's page and make last <p> in the post and comment bodies get the margin above it like the others (#219)
* Make last <p> in post body get padding above it

* When going to a comment from a user's page jump to it on page load
2024-09-27 00:44:32 -04:00
6e2e679a0e chore(oauth): add additional logging to login routine 2024-09-26 15:06:39 -04:00
6b44c1abf2 chore(oauth): add additional logging to login routine 2024-09-26 15:04:36 -04:00
a807002ddf Fix #206 and make (most) emotes embed in comments (#209)
* Fix links not being converted when multiple emojis are in one comment

* Make (most) emotes embed within comments

* Restore the behavior that the "rewrite_urls_removes_backslashes_and_rewrites_url" test looks for

* Listen to cargo fmt and cargo clippy's suggestions as well as removing some leftover comments and code

---------

Co-authored-by: Matthew Esposito <matt@matthew.science>
2024-09-25 13:36:23 -04:00
403513ac4c fix(search): handle queries' urlencoding (#264)
* fix(search): handle queries' urlencoding

* fix(search): handle queries' urlencoding
2024-09-24 23:30:06 -04:00
72f7d9d08c fix(search): handle multi-sub search (#263) 2024-09-24 23:20:12 -04:00
e6273e2ed5 fix(client): catch json suspended user error (#262)
* fix(client): catch json suspended user error
2024-09-24 23:13:36 -04:00
f1d4e6a417 fix(client): catch various json errors to properly render error page (#261)
* fix(client): catch various json errors to properly render error page

* fix(client): catch various json errors to properly render error page
2024-09-24 23:01:28 -04:00
e0d7837c02 fix(client): don't catch network policy errors, since they indicate q… (#259) 2024-09-24 21:45:47 -04:00
2d6ac78acf chore(client): update new oauth path (#258) 2024-09-24 21:28:54 -04:00
1e54c639d3 fix(client): add a timeout and retry logic to oauth daemon (#256)
* fix(client): add a timeout and retry logic to oauth daemon

* fix(client): add a timeout and retry logic to oauth daemon
2024-09-24 21:02:12 -04:00
d5f137ce47 fix(funding): add sponsor link 2024-09-21 15:48:19 -04:00
245fd9d408 fix(funding): update funding 2024-09-21 15:47:44 -04:00
b54620b5aa fix(client): use async_recursion crate 2024-09-21 15:44:27 -04:00
69c7a69afd add description for rss item (just like https://news.ycombinator.com/… (#220)
fix #201
2024-09-21 00:05:32 -04:00
2991813c2d Make poll results appear inside of a post (#218) 2024-09-21 00:02:34 -04:00
7156be6ad0 fix(client): fix failing tests, retries for canonical_path 2024-09-20 23:57:18 -04:00
f9b1a832c4 .dockerignore 2024-09-19 14:17:02 +12:00
1ffbce1ad8 Merge remote-tracking branch 'upstream/main' 2024-09-19 14:14:17 +12:00
793047f63f fix(client): revert to hyper-rustls=0.24.2 2024-09-18 11:24:00 -04:00
3625fdfdbe fix(ci): temporarily disable README updates 2024-09-17 14:42:09 -04:00
28f85f2599 Attempting to fix main-docker.yml for downloading digests 2024-09-17 14:39:42 -04:00
7be29f609c Attempting to fix main-docker.yml for downloading digests (#243)
* Update main-docker.yml (digests)

Updated digest name on upload (as per 1187084)

* Update main-docker.yml (digests download)

Trying another fix based on the template provided here https://github.com/actions/download-artifact for downloading multiple (filtered) Artifacts to the same directory
2024-09-17 14:37:39 -04:00
f18d135045 Update main-docker.yml (digests) (#238)
Updated digest name on upload (as per 1187084)
2024-09-17 14:09:32 -04:00
f2c410f9ea Merge remote-tracking branch 'upstream/main' 2024-09-17 12:28:21 +12:00
8ef45456d6 actions trigger 2024-09-16 16:39:07 -04:00
118708400a fix(ci): unique name 2024-09-16 16:36:24 -04:00
28e72c9058 fix(ci): bump versions (#234) 2024-09-16 16:29:52 -04:00
0b15250cc8 fix(oauth): catch network policy violation and rate limit (#233) 2024-09-16 16:16:08 -04:00
408ebe6ef1 feat(post): add archive.is link for link posts (#165) 2024-09-05 12:00:09 -04:00
Pim
7a0ea1fbd3 fix: spoiler hover and video size (#196)
* fix: unblur both media as body when hoevering over either one

* fix: set min size for video

when the video is not loaded, the size is determined by the poster image. But when the poster image returns a 404, then the video had a size of 0x0

* chore: tab/space
2024-09-05 11:59:43 -04:00
db8b92ea55 Fix a whole bunch of styling bugs (#193)
* Fix a whole bunch of mobile styling bugs

* Make searchbox scroll fix only apply in mobile mode to prevent bug

* Remove the min-width requirement for the main column

This was meant to be removed already, this is what fixes posts having an odd right side gap before swapping to the mobile layout

* Make margins consistent between fixed and unfixed navbar settings

* Remove some empty space from deleted option

* Make mobile layout post width fix only apply in mobile mode to prevent bug

* Make sure some options only get applied to the elements that need them, also fix the margins on the settings page

* Move search comments option before it starts touching the sort options and wrapping the x amount of comments text

* Trigger the even further compacted layout a little earlier, right before text begins wrapping in odd ways

* In the extra small mobile layout make give up/downvote numbers enough room so they aren't clipping out of their box

* Fix https://github.com/redlib-org/redlib/issues/172

* Properly center search box instead of having it slightly skewed

* Undo word wrapping since it breaks the sorting options and the only other viable setting has an absolute conniption on Chrome for some reason

* Readd word wrapping and just force it to normal for the sorting section

* Make post flair line up with title

* Make post flair position consistent

* Make footer text properly horizontally centered in mobile mode and fix slight vertical misalignment issues

* Make feeds button appear in settings menu to keep navbar looking consistent

* Fix extra navbar padding on search page

* Reduce gap between navbar and content in mobile mode

* Reduce gap between navbar and content in mobile mode
2024-09-05 11:59:21 -04:00
c494fbec31 Insert noindex meta for ROBOTS_DISABLE_INDEXING (#199) (#207) 2024-09-03 19:44:04 -04:00
438e412be3 Add anchor to comment link (#212) 2024-09-03 19:21:33 -04:00
d0e081e6a0 chore(deps): bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows (#214)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 19:21:11 -04:00
041ecceeaf Update license tag (#200) 2024-08-07 14:09:54 -04:00
3677ca10e2 Fix sort options overflow on small screens (#192) 2024-07-25 20:26:27 -04:00
21fd34710c Fix font sizes in search bar being incorrect (#190) 2024-07-24 19:56:06 -04:00
4205959ade fix(docs): Add env HIDE_SIDEBAR_AND_SUMMARY (#188) 2024-07-23 21:02:15 -04:00
27b56c1781 fix(client): handle new gated and quarantined error types (#187)
* fix(client): handle new gated and quarantined error types

* test(client): add test for gated and quarantined
2024-07-21 14:22:54 -04:00
374238abc3 Add RSS feeds (fix #57) (#90)
* Add RSS feeds

* feat(rss): feature-ify rss

* feat(rss): config-ify rss

* fix(rss): update info page

* feat(rss): conditionally add RSS feeds to user and sub pages

* feat(rss): implement URLs for RSS
2024-07-21 14:09:34 -04:00
Pim
9bdb5c8966 feat: also blur text in post body for spoilers (#186)
chore: simplify blur classes
2024-07-21 10:22:40 -04:00
410872d988 Make search bar responsive on mobile devices (#178)
* Search: Apply bg on elements rather than container

This changes allows moving the individual elements that composes
the search bar around without losing the background on the elements.

* Update search widget semantic structure

* Make search bar design responsive on small screens

* Fix border color

* Polish
2024-07-17 21:28:50 -04:00
c890e809b7 Improve post information widget of user comments for devices under 480px width (#183)
* Fix user comment post link disappearing when < 480px

* Improve user comment metadata design for mobile

* Remove formerly unused CSS class style

The prior commit introduced the usage of the `comment_subreddit` class
to identify the subreddit that the reddit user posted the comment on.

However, this class came with a legacy style within the CSS file that was
previously not used anywhere within the current day Redlib

As such this style has been removed.
2024-07-17 21:27:13 -04:00
37 changed files with 2541 additions and 1376 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target

5
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
liberapay: spike liberapay: sigaloid
custom: ['https://www.buymeacoffee.com/spikecodes'] buy_me_a_coffee: sigaloid
github: sigaloid

View File

@ -7,6 +7,10 @@ assignees: ''
--- ---
<!--
BEFORE FILING A BUG REPORT: Ensure that you are running the latest git commit. Visit /info on your instance, and ensure the git commit listed is the same commit listed on the home page.
-->
## Describe the bug ## Describe the bug
<!-- <!--
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@ -31,3 +35,7 @@ Steps to reproduce the behavior:
<!-- <!--
Add any other context about the problem here. Add any other context about the problem here.
--> -->
<!-- Mandatory -->
- [ ] I checked that the instance that this was reported on is running the latest git commit, or I can reproduce it locally on the latest git commit

View File

@ -15,15 +15,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- { platform: linux/amd64, target: x86_64-unknown-linux-musl} - { platform: linux/amd64, target: x86_64-unknown-linux-musl }
- { platform: linux/arm64, target: aarch64-unknown-linux-musl} - { platform: linux/arm64, target: aarch64-unknown-linux-musl }
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf} - { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf }
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- - name: Docker meta
name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
@ -31,21 +29,17 @@ jobs:
tags: | tags: |
type=sha type=sha
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- - name: Set up QEMU
name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 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 uses: docker/login-action@v3
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }} password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- - name: Build and push
name: Build and push
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
@ -55,17 +49,15 @@ jobs:
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
file: Dockerfile file: Dockerfile
build-args: TARGET=${{ matrix.target }} build-args: TARGET=${{ matrix.target }}
- - name: Export digest
name: Export digest
run: | run: |
mkdir -p /tmp/digests mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}" digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- - name: Upload digest
name: Upload digest uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with: with:
name: digests name: digests-${{ matrix.target }}
path: /tmp/digests/* path: /tmp/digests/*
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
@ -74,17 +66,16 @@ jobs:
needs: needs:
- build - build
steps: steps:
- - name: Download digests
name: Download digests uses: actions/download-artifact@v4.1.7
uses: actions/download-artifact@v3
with: with:
name: digests
path: /tmp/digests path: /tmp/digests
- pattern: digests-*
name: Set up Docker Buildx merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - name: Docker meta
name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
@ -92,31 +83,27 @@ jobs:
tags: | tags: |
type=sha type=sha
type=raw,value=latest,enable={{is_default_branch}} 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 uses: docker/login-action@v3
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }} password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- - name: Create manifest list and push
name: Create manifest list and push
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Push README to Quay.io # - name: Push README to Quay.io
uses: christian-korneck/update-container-description-action@v1 # uses: christian-korneck/update-container-description-action@v1
env: # env:
DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }} # DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
with: # with:
destination_container_repo: quay.io/redlib/redlib # destination_container_repo: quay.io/redlib/redlib
provider: quay # provider: quay
readme_file: 'README.md' # readme_file: 'README.md'
- - name: Inspect image
name: Inspect image
run: | run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}

View File

@ -56,7 +56,7 @@ jobs:
- name: Calculate SHA256 checksum - name: Calculate SHA256 checksum
run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256 run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: Upload a Build Artifact name: Upload a Build Artifact
with: with:
name: redlib name: redlib

476
Cargo.lock generated
View File

@ -30,6 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@ -78,42 +79,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]] [[package]]
name = "askama" name = "async-recursion"
version = "0.12.1" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"askama_derive",
"askama_escape",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"mime",
"mime_guess",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
] ]
[[package]] [[package]]
@ -124,7 +97,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
]
[[package]]
name = "atom_syndication"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
dependencies = [
"chrono",
"derive_builder 0.20.0",
"diligent-date-parser",
"never",
"quick-xml 0.31.0",
] ]
[[package]] [[package]]
@ -148,6 +134,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -171,9 +163,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "6.0.0" version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@ -223,7 +215,7 @@ dependencies = [
"cached_proc_macro", "cached_proc_macro",
"cached_proc_macro_types", "cached_proc_macro_types",
"futures", "futures",
"hashbrown", "hashbrown 0.14.5",
"instant", "instant",
"once_cell", "once_cell",
"thiserror", "thiserror",
@ -236,10 +228,10 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669" checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669"
dependencies = [ dependencies = [
"darling", "darling 0.20.9",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -260,6 +252,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.7" version = "4.5.7"
@ -348,14 +349,38 @@ dependencies = [
"typenum", "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]] [[package]]
name = "darling" name = "darling"
version = "0.20.9" version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.9",
"darling_macro", "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]] [[package]]
@ -368,8 +393,19 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim 0.11.1",
"syn", "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]] [[package]]
@ -378,9 +414,9 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.9",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -398,6 +434,68 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -408,12 +506,30 @@ dependencies = [
"crypto-common", "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]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 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]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.2" version = "0.10.2"
@ -613,6 +729,12 @@ dependencies = [
"allocator-api2", "allocator-api2",
] ]
[[package]]
name = "hashbrown"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -685,9 +807,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.25.0" version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http", "http",
@ -695,7 +817,6 @@ dependencies = [
"log", "log",
"rustls", "rustls",
"rustls-native-certs", "rustls-native-certs",
"rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
] ]
@ -723,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.14.5",
] ]
[[package]] [[package]]
@ -735,6 +856,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "inventory"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.12" version = "0.4.12"
@ -778,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d"
dependencies = [ dependencies = [
"core2", "core2",
"hashbrown", "hashbrown 0.14.5",
"rle-decode-fast", "rle-decode-fast",
] ]
@ -862,6 +989,12 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -878,6 +1011,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 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]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -912,6 +1054,18 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "once_map"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed29bb6f7d6ac14023acb332a356f3891265d780e254057c866dbe7a909d2d2d"
dependencies = [
"ahash",
"hashbrown 0.15.0",
"parking_lot",
"stable_deref_trait",
]
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.5"
@ -1002,6 +1156,26 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 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]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.36"
@ -1047,11 +1221,11 @@ dependencies = [
[[package]] [[package]]
name = "redsunlib" name = "redsunlib"
version = "0.35.2" version = "0.35.3"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"askama", "async-recursion",
"base64", "base64 0.22.1",
"brotli", "brotli",
"build_html", "build_html",
"cached", "cached",
@ -1069,11 +1243,14 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pretty_env_logger", "pretty_env_logger",
"regex", "regex",
"rinja",
"route-recognizer", "route-recognizer",
"rss",
"rust-embed", "rust-embed",
"sealed_test", "sealed_test",
"serde", "serde",
"serde_json", "serde_json",
"serde_json_path",
"serde_yaml", "serde_yaml",
"time", "time",
"tokio", "tokio",
@ -1126,6 +1303,43 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rinja"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc"
dependencies = [
"itoa",
"rinja_derive",
]
[[package]]
name = "rinja_derive"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede"
dependencies = [
"memchr",
"mime",
"mime_guess",
"once_map",
"proc-macro2",
"quote",
"rinja_parser",
"rustc-hash",
"syn 2.0.68",
]
[[package]]
name = "rinja_parser"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941"
dependencies = [
"memchr",
"nom",
]
[[package]] [[package]]
name = "rle-decode-fast" name = "rle-decode-fast"
version = "1.0.3" version = "1.0.3"
@ -1138,6 +1352,18 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" 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]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.4.0" version = "8.4.0"
@ -1158,7 +1384,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn", "syn 2.0.68",
"walkdir", "walkdir",
] ]
@ -1179,6 +1405,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.34"
@ -1194,55 +1426,44 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.22.4" version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [ dependencies = [
"log", "log",
"ring", "ring",
"rustls-pki-types",
"rustls-webpki", "rustls-webpki",
"subtle", "sct",
"zeroize",
] ]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.7.0" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pemfile", "rustls-pemfile",
"rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework",
] ]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "2.1.2" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [ dependencies = [
"base64", "base64 0.21.7",
"rustls-pki-types",
] ]
[[package]]
name = "rustls-pki-types"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.102.4" version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types",
"untrusted", "untrusted",
] ]
@ -1288,6 +1509,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "sealed_test" name = "sealed_test"
version = "1.1.0" version = "1.1.0"
@ -1307,7 +1538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6" checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1350,7 +1581,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1364,6 +1595,59 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_json_path"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba"
dependencies = [
"inventory",
"nom",
"once_cell",
"regex",
"serde",
"serde_json",
"serde_json_path_core",
"serde_json_path_macros",
"thiserror",
]
[[package]]
name = "serde_json_path_core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79"
dependencies = [
"inventory",
"once_cell",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "serde_json_path_macros"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723"
dependencies = [
"inventory",
"once_cell",
"serde_json_path_core",
"serde_json_path_macros_internal",
]
[[package]]
name = "serde_json_path_macros_internal"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.6" version = "0.6.6"
@ -1437,6 +1721,18 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -1444,10 +1740,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "syn"
version = "2.6.1" version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
@ -1498,7 +1799,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1576,17 +1877,16 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.25.0" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-pki-types",
"tokio", "tokio",
] ]
@ -1950,11 +2250,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.68",
] ]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

View File

@ -1,9 +1,9 @@
[package] [package]
name = "redsunlib" name = "redsunlib"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0" license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib" repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.2" version = "0.35.3"
authors = [ authors = [
"Matthew Esposito <matt+cargo@matthew.science>", "Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>", "spikecodes <19519553+spikecodes@users.noreply.github.com>",
@ -11,7 +11,7 @@ authors = [
edition = "2021" edition = "2021"
[dependencies] [dependencies]
askama = { version = "0.12.1", default-features = false } rinja = { version = "0.3.4", default-features = false }
cached = { version = "0.51.3", features = ["async"] } cached = { version = "0.51.3", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [ clap = { version = "4.4.11", default-features = false, features = [
"std", "std",
@ -22,7 +22,7 @@ serde = { version = "1.0.193", features = ["derive"] }
cookie = "0.18.0" cookie = "0.18.0"
futures-lite = "2.2.0" futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] } hyper = { version = "0.14.28", features = ["full"] }
hyper-rustls = "0.25.0" hyper-rustls = "0.24.2"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
route-recognizer = "0.3.1" route-recognizer = "0.3.1"
serde_json = "1.0.108" serde_json = "1.0.108"
@ -31,7 +31,7 @@ time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0" url = "2.5.0"
rust-embed = { version = "8.1.0", features = ["include-exclude"] } rust-embed = { version = "8.1.0", features = ["include-exclude"] }
libflate = "2.0.0" libflate = "2.0.0"
brotli = { version = "6.0.0", features = ["std"] } brotli = { version = "7.0.0", features = ["std"] }
toml = "0.8.8" toml = "0.8.8"
once_cell = "1.19.0" once_cell = "1.19.0"
serde_yaml = "0.9.29" serde_yaml = "0.9.29"
@ -42,7 +42,11 @@ fastrand = "2.0.1"
log = "0.4.20" log = "0.4.20"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1" arc-swap = "1.7.1"
serde_json_path = "0.6.7"
async-recursion = "1.1.1"
[dev-dependencies] [dev-dependencies]
lipsum = "0.9.0" lipsum = "0.9.0"

View File

@ -71,7 +71,7 @@ And at the time, I was reading an excerpt from Mao Zedong, so the name seemed ap
- [Rust](https://www.rust-lang.org/) - Programming language - [Rust](https://www.rust-lang.org/) - Programming language
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client - [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
- [Askama](https://github.com/djc/askama) - Templating engine - [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
- [Rustls](https://github.com/rustls/rustls) - TLS library - [Rustls](https://github.com/rustls/rustls) - TLS library
## How is it different from other Reddit front ends? ## How is it different from other Reddit front ends?
@ -312,14 +312,15 @@ Assign a default value for each instance-specific setting by passing environment
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. | | `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. | | `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. | | `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 ## 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. Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value | | Name | Possible values | Default value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` | | `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ | | `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` | | `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` | | `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
@ -337,4 +338,5 @@ Assign a default value for each user-modifiable setting by passing environment v
| `HIDE_AWARDS` | `["on", "off"]` | `off` | | `HIDE_AWARDS` | `["on", "off"]` | `off` |
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` | | `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
| `HIDE_SCORE` | `["on", "off"]` | `off` | | `HIDE_SCORE` | `["on", "off"]` | `off` |
| `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` | | `FIXED_NAVBAR` | `["on", "off"]` | `on` |

View File

@ -62,7 +62,7 @@
"REDLIB_BANNER": { "REDLIB_BANNER": {
"required": false "required": false
}, },
"REDLIB_ROBOTS_DISABLE_INDEXING": { "REDLIB_ROBOTS_DISABLE_INDEXING": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_SUBSCRIPTIONS": { "REDLIB_DEFAULT_SUBSCRIPTIONS": {
@ -76,6 +76,12 @@
}, },
"REDLIB_PUSHSHIFT_FRONTEND": { "REDLIB_PUSHSHIFT_FRONTEND": {
"required": false "required": false
},
"REDLIB_ENABLE_RSS": {
"required": false
},
"REDLIB_FULL_URL": {
"required": false
} }
} }
} }

View File

@ -14,6 +14,6 @@ PORT=12345
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off #REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off #REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off #REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3) #REDLIB_DEFAULT_SUBSCRIPTIONS=(sub1+sub2+sub3)
#REDLIB_DEFAULT_HIDE_AWARDS=off #REDLIB_DEFAULT_HIDE_AWARDS=off
#REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off #REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off

View File

@ -30,7 +30,8 @@ RestrictNamespaces=yes
RestrictRealtime=yes RestrictRealtime=yes
RestrictSUIDSGID=yes RestrictSUIDSGID=yes
SystemCallArchitectures=native SystemCallArchitectures=native
SystemCallFilter=@system-service ~@privileged ~@resources SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
UMask=0077 UMask=0077
[Install] [Install]

View File

@ -22,15 +22,16 @@ use crate::server::RequestExt;
use crate::utils::format_url; use crate::utils::format_url;
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com"; const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new() let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
.with_native_roots()
.expect("No native root certificates found")
.https_only()
.enable_http1()
.build();
client::Client::builder().build(https) client::Client::builder().build(https)
}); });
@ -44,6 +45,11 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false); pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
static URL_PAIRS: [(&str, &str); 2] = [
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
];
/// Gets the canonical path for a resource on Reddit. This is accomplished by /// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`. /// making a `HEAD` request to Reddit at the path given in `path`.
/// ///
@ -57,13 +63,32 @@ pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
/// `Location` header. An `Err(String)` is returned if Reddit responds with a /// `Location` header. An `Err(String)` is returned if Reddit responds with a
/// 429, or if we were unable to decode the value in the `Location` header. /// 429, or if we were unable to decode the value in the `Location` header.
#[cached(size = 1024, time = 600, result = true)] #[cached(size = 1024, time = 600, result = true)]
pub async fn canonical_path(path: String) -> Result<Option<String>, String> { #[async_recursion::async_recursion]
let res = reddit_head(path.clone(), true).await?; pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
if tries == 0 {
return Ok(None);
}
// for each URL pair, try the HEAD request
let res = {
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
let mut res = None;
for (url_base, url_base_host) in URL_PAIRS {
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
if let Some(res) = &res {
if !res.status().is_client_error() {
break;
}
}
}
res
};
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
let status = res.status().as_u16(); let status = res.status().as_u16();
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
match status { match status {
429 => Err("Too many requests.".to_string()),
// If Reddit responds with a 2xx, then the path is already canonical. // If Reddit responds with a 2xx, then the path is already canonical.
200..=299 => Ok(Some(path)), 200..=299 => Ok(Some(path)),
@ -73,6 +98,7 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
let Ok(original) = val.to_str() else { let Ok(original) = val.to_str() else {
return Err("Unable to decode Location header.".to_string()); return Err("Unable to decode Location header.".to_string());
}; };
// We need to strip the .json suffix from the original path. // We need to strip the .json suffix from the original path.
// In addition, we want to remove share parameters. // In addition, we want to remove share parameters.
// Cut it off here instead of letting it propagate all the way // Cut it off here instead of letting it propagate all the way
@ -85,7 +111,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// also remove all Reddit domain parts with format_url. // also remove all Reddit domain parts with format_url.
// Otherwise, it will literally redirect to Reddit.com. // Otherwise, it will literally redirect to Reddit.com.
let uri = format_url(stripped_uri); let uri = format_url(stripped_uri);
Ok(Some(uri))
// Decrement tries and try again
canonical_path(uri, tries - 1).await
} }
None => Ok(None), None => Ok(None),
}, },
@ -94,6 +122,12 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// as above), return a None. // as above), return a None.
300..=399 => Ok(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( _ => Ok(
res res
.headers() .headers()
@ -160,20 +194,26 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP /// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
/// 3xx codes Reddit returns and will automatically redirect. /// 3xx codes Reddit returns and will automatically redirect.
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
request(&Method::GET, path, true, quarantine) request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
} }
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects. /// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
request(&Method::HEAD, path, false, quarantine) request(&Method::HEAD, path, false, quarantine, base_path, host)
} }
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
// request(&Method::HEAD, path, false, quarantine, false)
// }
// Unused - reddit_head is only ever called in the context of a short URL
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect` /// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
/// will recurse on the URL that Reddit provides in the Location HTTP header /// will recurse on the URL that Reddit provides in the Location HTTP header
/// in its response. /// in its response.
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
// Build Reddit URL from path. // Build Reddit URL from path.
let url = format!("{REDDIT_URL_BASE}{path}"); let url = format!("{base_path}{path}");
// Construct the hyper client from the HTTPS connector. // Construct the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone(); let client: Client<_, Body> = CLIENT.clone();
@ -198,7 +238,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.header("Client-Vendor-Id", vendor_id) .header("Client-Vendor-Id", vendor_id)
.header("X-Reddit-Device-Id", device_id) .header("X-Reddit-Device-Id", device_id)
.header("x-reddit-loid", loid) .header("x-reddit-loid", loid)
.header("Host", "oauth.reddit.com") .header("Host", host)
.header("Authorization", &format!("Bearer {token}")) .header("Authorization", &format!("Bearer {token}"))
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" }) .header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5") .header("Accept-Language", "en-US,en;q=0.5")
@ -253,6 +293,8 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.to_string(), .to_string(),
true, true,
quarantine, quarantine,
base_path,
host,
) )
.await; .await;
}; };
@ -374,6 +416,16 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
match serde_json::from_reader(body.reader()) { match serde_json::from_reader(body.reader()) {
Ok(value) => { Ok(value) => {
let json: Value = value; let json: Value = value;
// If user is suspended
if let Some(data) = json.get("data") {
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
if is_suspended {
return Err("suspended".into());
}
}
}
// If Reddit returned an error // If Reddit returned an error
if json["error"].is_i64() { if json["error"].is_i64() {
// OAuth token has expired; http status 401 // OAuth token has expired; http status 401
@ -382,6 +434,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await; let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string()); return Err("OAuth token has expired. Please refresh the page!".to_string());
} }
// Handle quarantined
if json["reason"] == "quarantined" {
return Err("quarantined".into());
}
// Handle gated
if json["reason"] == "gated" {
return Err("gated".into());
}
// Handle private subs
if json["reason"] == "private" {
return Err("private".into());
}
// Handle banned subs
if json["reason"] == "banned" {
return Err("banned".into());
}
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"])) Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else { } else {
Ok(json) Ok(json)
@ -417,13 +487,34 @@ async fn test_localization_popular() {
async fn test_obfuscated_share_link() { async fn test_obfuscated_share_link() {
let share_link = "/r/rust/s/kPgq8WNHRK".into(); let share_link = "/r/rust/s/kPgq8WNHRK".into();
// Correct link without share parameters // Correct link without share parameters
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into(); let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link))); assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_share_link_strip_json() { async fn test_share_link_strip_json() {
let link = "/17krzvz".into(); let link = "/17krzvz".into();
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into(); let canonical_link = "/comments/17krzvz".into();
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link))); assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_private_sub() {
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("private".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_banned_sub() {
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("banned".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_gated_sub() {
// quarantine to false to specifically catch when we _don't_ catch it
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
assert!(link.is_err());
assert_eq!(link, Err("gated".into()));
} }

View File

@ -111,6 +111,12 @@ pub struct Config {
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")] #[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")] #[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>, 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 { impl Config {
@ -158,6 +164,8 @@ impl Config {
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"), pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
} }
} }
} }
@ -187,6 +195,8 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(), "REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(), "REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
"REDLIB_FULL_URL" => config.full_url.clone(),
_ => None, _ => None,
} }
} }

View File

@ -5,8 +5,8 @@ use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine}; use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences}; use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use serde_json::Value; use serde_json::Value;
use std::borrow::ToOwned; use std::borrow::ToOwned;
use std::collections::HashSet; use std::collections::HashSet;

View File

@ -3,10 +3,10 @@ use crate::{
server::RequestExt, server::RequestExt,
utils::{ErrorTemplate, Preferences}, utils::{ErrorTemplate, Preferences},
}; };
use askama::Template;
use build_html::{Container, Html, HtmlContainer, Table}; use build_html::{Container, Html, HtmlContainer, Table};
use hyper::{http::Error, Body, Request, Response}; use hyper::{http::Error, Body, Request, Response};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rinja::Template;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode], ["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)], ["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)], ["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 //TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
]) ])
.with_header_row(["Settings"]), .with_header_row(["Settings"]),
@ -167,6 +169,8 @@ impl InstanceInfo {
Compile mode: {}\n Compile mode: {}\n
SFW only: {:?}\n SFW only: {:?}\n
Pushshift frontend: {:?}\n Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n Config:\n
Banner: {:?}\n Banner: {:?}\n
Hide awards: {:?}\n Hide awards: {:?}\n
@ -193,6 +197,8 @@ impl InstanceInfo {
self.deploy_unix_ts, self.deploy_unix_ts,
self.compile_mode, self.compile_mode,
self.config.sfw_only, self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift, self.config.pushshift,
self.config.banner, self.config.banner,
self.config.default_hide_awards, self.config.default_hide_awards,

View File

@ -284,6 +284,9 @@ async fn main() {
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed()); app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed()); app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed()); app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
app
.at("/emote/:subreddit_id/:filename")
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
app app
.at("/preview/:loc/award_images/:fullname/:id") .at("/preview/:loc/award_images/:fullname/:id")
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed()); .get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
@ -299,6 +302,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed()); app.at("/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/[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").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").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()); app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -313,6 +317,9 @@ async fn main() {
// Mascots // Mascots
app.at("/mascot/:name").get(|r| mascot_image(r).boxed()); 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 // Subreddit services
app app
.at("/r/:sub") .at("/r/:sub")
@ -385,7 +392,7 @@ async fn main() {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
match req.param("id").as_deref() { match req.param("id").as_deref() {
// Share link // Share link
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).await { Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
Ok(Some(path)) => Ok(redirect(&path)), Ok(Some(path)) => Ok(redirect(&path)),
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
Err(e) => error(req, &e).await, Err(e) => error(req, &e).await,
@ -404,7 +411,7 @@ async fn main() {
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
// Short link for post // Short link for post
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}")).await { Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
Ok(path_opt) => match path_opt { Ok(path_opt) => match path_opt {
Some(path) => Ok(redirect(&path)), Some(path) => Ok(redirect(&path)),
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,

View File

@ -6,13 +6,14 @@ use crate::{
}; };
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use hyper::{client, Body, Method, Request}; use hyper::{client, Body, Method, Request};
use log::{info, trace}; use log::{error, info, trace};
use serde_json::json; use serde_json::json;
use tokio::time::{error::Elapsed, timeout};
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
static AUTH_ENDPOINT: &str = "https://accounts.reddit.com"; static AUTH_ENDPOINT: &str = "https://www.reddit.com";
// Spoofed client for Android devices // Spoofed client for Android devices
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -25,11 +26,32 @@ pub struct Oauth {
} }
impl Oauth { impl Oauth {
/// Create a new OAuth client
pub(crate) async fn new() -> Self { pub(crate) async fn new() -> Self {
let mut oauth = Self::default(); // Call new_internal until it succeeds
oauth.login().await; loop {
oauth let attempt = Self::new_with_timeout().await;
match attempt {
Ok(Some(oauth)) => {
info!("[✅] Successfully created OAuth client");
return oauth;
}
Ok(None) => {
error!("Failed to create OAuth client. Retrying in 5 seconds...");
continue;
}
Err(duration) => {
error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds...");
}
}
}
} }
async fn new_with_timeout() -> Result<Option<Self>, Elapsed> {
let mut oauth = Self::default();
timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth))
}
pub(crate) fn default() -> Self { pub(crate) fn default() -> Self {
// Generate a device to spoof // Generate a device to spoof
let device = Device::new(); let device = Device::new();
@ -46,7 +68,7 @@ impl Oauth {
} }
async fn login(&mut self) -> Option<()> { async fn login(&mut self) -> Option<()> {
// Construct URL for OAuth token // Construct URL for OAuth token
let url = format!("{AUTH_ENDPOINT}/api/access_token"); let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid");
let mut builder = Request::builder().method(Method::POST).uri(&url); let mut builder = Request::builder().method(Method::POST).uri(&url);
// Add headers from spoofed client // Add headers from spoofed client
@ -69,13 +91,19 @@ impl Oauth {
// Build request // Build request
let request = builder.body(body).unwrap(); let request = builder.body(body).unwrap();
trace!("Sending token request...");
// Send request // Send request
let client: client::Client<_, Body> = CLIENT.clone(); let client: client::Client<_, Body> = CLIENT.clone();
let resp = client.request(request).await.ok()?; let resp = client.request(request).await.ok()?;
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes. // Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
// Technically it's not needed, but it's easy for Reddit API to check for this. // Technically it's not needed, but it's easy for Reddit API to check for this.
// It's some kind of header that uniquely identifies the device. // It's some kind of header that uniquely identifies the device.
// Not worried about the privacy implications, since this is randomly changed
// and really only as privacy-concerning as the OAuth token itself.
if let Some(header) = resp.headers().get("x-reddit-loid") { if let Some(header) = resp.headers().get("x-reddit-loid") {
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string()); self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
} }
@ -85,10 +113,14 @@ impl Oauth {
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string()); self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
} }
trace!("Serializing response...");
// Serialize response // Serialize response
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
trace!("Accessing relevant fields...");
// Save token and expiry // Save token and expiry
self.token = json.get("access_token")?.as_str()?.to_string(); self.token = json.get("access_token")?.as_str()?.to_string();
self.expires_in = json.get("expires_in")?.as_u64()?; self.expires_in = json.get("expires_in")?.as_u64()?;

View File

@ -4,14 +4,14 @@ use crate::config::get_setting;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine}; use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{ use crate::utils::{
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences, error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_emotes, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
}; };
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use askama::Template;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::collections::HashSet; use rinja::Template;
use std::collections::{HashMap, HashSet};
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
@ -72,11 +72,15 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default()); return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
} }
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) { let query_body = match COMMENT_SEARCH_CAPTURE.captures(&url) {
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "), Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
None => String::new(), None => String::new(),
}; };
let query_string = format!("q={query_body}&type=comment");
let form = url::form_urlencoded::parse(query_string.as_bytes()).collect::<HashMap<_, _>>();
let query = form.get("q").unwrap().clone().to_string();
let comments = match query.as_str() { let comments = match query.as_str() {
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req), "" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req), _ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
@ -174,7 +178,7 @@ fn build_comment(
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)), get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
) )
} else { } else {
rewrite_urls(&val(comment, "body_html")) rewrite_emotes(&data["media_metadata"], val(comment, "body_html"))
}; };
let kind = comment["kind"].as_str().unwrap_or_default().to_string(); let kind = comment["kind"].as_str().unwrap_or_default().to_string();

View File

@ -5,10 +5,10 @@ use crate::{
subreddit::{can_access_quarantine, quarantine}, subreddit::{can_access_quarantine, quarantine},
RequestExt, RequestExt,
}; };
use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rinja::Template;
// STRUCTS // STRUCTS
struct SearchParams { struct SearchParams {
@ -60,7 +60,8 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
} else { } else {
"" ""
}; };
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); let uri_path = req.uri().path().replace("+", "%2B");
let path = format!("{}.json?{}{}&raw_json=1", uri_path, req.uri().query().unwrap_or_default(), nsfw_results);
let mut query = param(&path, "q").unwrap_or_default(); let mut query = param(&path, "q").unwrap_or_default();
query = REDDIT_URL_MATCH.replace(&query, "").to_string(); query = REDDIT_URL_MATCH.replace(&query, "").to_string();
@ -68,10 +69,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
return Ok(redirect("/")); return Ok(redirect("/"));
} }
if query.starts_with("r/") { if query.starts_with("r/") || query.starts_with("user/") {
return Ok(redirect(&format!("/{query}"))); return Ok(redirect(&format!("/{query}")));
} }
if query.starts_with("u/") {
return Ok(redirect(&format!("/user{}", &query[1..])));
}
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let quarantined = can_access_quarantine(&req, &sub); let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits // Handle random subreddits

View File

@ -3,10 +3,10 @@ use std::collections::HashMap;
// CRATES // CRATES
use crate::server::ResponseExt; use crate::server::ResponseExt;
use crate::utils::{redirect, template, Preferences}; use crate::utils::{redirect, template, Preferences};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use futures_lite::StreamExt; use futures_lite::StreamExt;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
// STRUCTS // STRUCTS

View File

@ -1,11 +1,12 @@
use crate::{config, utils};
// CRATES // CRATES
use crate::utils::{ 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, catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
}; };
use crate::{client::json, server::ResponseExt, RequestExt}; use crate::{client::json, server::ResponseExt, RequestExt};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
@ -459,8 +460,71 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
}) })
} }
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
description: Some(format!(
"<a href='{}{}'>Comments</a>",
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
post.permalink
)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() { async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await; let subreddit = subreddit("rust", false).await;
assert!(subreddit.is_ok()); 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());
}

View File

@ -2,8 +2,9 @@
use crate::client::json; use crate::client::json;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
use askama::Template; use crate::{config, utils};
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use time::{macros::format_description, OffsetDateTime}; use time::{macros::format_description, OffsetDateTime};
// STRUCTS // STRUCTS
@ -129,6 +130,56 @@ async fn user(name: &str) -> Result<User, String> {
}) })
} }
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use crate::utils::rewrite_urls;
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get user
let user_str = req.param("name").unwrap_or_default();
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
// Get path
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);
// Get user
let user_obj = user(&user_str).await.unwrap_or_default();
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(user_str)
.description(user_obj.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_fetching_user() { async fn test_fetching_user() {
let user = user("spez").await; let user = user("spez").await;

View File

@ -1,20 +1,22 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::config::get_setting; use crate::config::{self, get_setting};
// //
// CRATES // CRATES
// //
use crate::{client::json, server::RequestExt}; use crate::{client::json, server::RequestExt};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use log::error; use log::error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rinja::Template;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use serde_json::Value; use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime}; use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url; use url::Url;
@ -327,6 +329,7 @@ pub struct Post {
pub gallery: Vec<GalleryMedia>, pub gallery: Vec<GalleryMedia>,
pub awards: Awards, pub awards: Awards,
pub nsfw: bool, pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String, pub ws_url: String,
} }
@ -435,6 +438,7 @@ impl Post {
awards, awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"), 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())) Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
@ -788,6 +792,7 @@ pub async fn parse_post(post: &Value) -> Post {
awards, awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"), ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
} }
} }
@ -933,12 +938,19 @@ pub fn rewrite_urls(input_text: &str) -> String {
// Rewrite Reddit links to Redlib // Rewrite Reddit links to Redlib
REDDIT_REGEX.replace_all(input_text, r#"href="/"#) REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
.to_string(); .to_string();
text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default())) loop {
.to_string() if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
// Remove (html-encoded) "\" from URLs. break;
.replace("%5C", "") } else {
.replace("\\_", "_"); text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
}
}
// Remove (html-encoded) "\" from URLs.
text1 = text1.replace("%5C", "").replace("\\_", "_");
// Rewrite external media previews to Redlib // Rewrite external media previews to Redlib
loop { loop {
@ -994,6 +1006,83 @@ pub fn rewrite_urls(input_text: &str) -> String {
} }
} }
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
/* Create the paths we'll use to look for our data inside the json.
Because we don't know the name of any given emote we use a wildcard to parse them. */
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
// Extract all of the results from those json paths
let link_nodes = media_metadata.json_path(&link_path);
let id_nodes = media_metadata.json_path(&id_path);
// Initialize our vectors
let mut id_vec = Vec::new();
let mut link_vec = Vec::new();
// Add the relevant data to each of our vectors so we can access it by number later
for current_id in id_nodes {
id_vec.push(current_id)
}
for current_link in link_nodes {
link_vec.push(current_link)
}
/* Set index to the length of link_vec.
This is one larger than we'll actually be looking at, but we correct that later */
let mut index = link_vec.len();
// Comment needs to be in scope for when we call rewrite_urls()
let mut comment = comment;
/* Loop until index hits zero.
This also prevents us from trying to do anything on an empty vector */
while index != 0 {
/* Subtract 1 from index to get the real index we should be looking at.
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
index -= 1;
// Convert our current index in id_vec into a string so we can search through it with regex
let current_id = id_vec[index].to_string();
/* The ID number can be multiple lengths, so we capture it with regex.
We also want to only attempt anything when we get matches to avoid panicking */
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(&current_id) {
// Format the ID to include the colons it has in the comment text
let id = format!(":{}:", &id_capture[1]);
// Convert current link to string to search through it with the regex
let link = link_vec[index].to_string();
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
It's a good idea and makes everything look nicer, so we'll do the same. */
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
let to_replace_with = format!(
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
&link_capture[1]
);
// Inside the comment replace the ID we found with the string that will embed the image
comment = comment.replace(&id, &to_replace_with).to_string();
}
}
}
// Call rewrite_urls() to transform any other Reddit links
rewrite_urls(&comment)
}
// Format vote count to a string that will be displayed. // Format vote count to a string that will be displayed.
// Append `m` and `k` for millions and thousands respectively, and // Append `m` and `k` for millions and thousands respectively, and
// round to the nearest tenth. // round to the nearest tenth.
@ -1100,6 +1189,28 @@ pub fn sfw_only() -> bool {
} }
} }
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
/// value `on`.
///
/// If this variable is set as such, the instance will block all robots in robots.txt and
/// insert the noindex, nofollow meta tag on every page.
pub fn disable_indexing() -> bool {
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
Some(val) => val == "on",
None => false,
}
}
// Determines if a request shoud redirect to a nsfw landing gate. // Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool { pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only(); let sfw_instance = sfw_only();
@ -1155,6 +1266,20 @@ pub fn url_path_basename(path: &str) -> String {
} }
} }
// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{format_num, format_url, rewrite_urls}; use super::{format_num, format_url, rewrite_urls};
@ -1279,3 +1404,11 @@ fn test_url_path_basename() {
// empty path // empty path
assert_eq!(url_path_basename("/"), ""); assert_eq!(url_path_basename("/"), "");
} }
#[test]
fn test_rewriting_emotes() {
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
/* Black theme setting */ /* Black theme setting */
.black { .black {
--accent: #009a9a; --accent: #bb2b3b;
--green: #00a229; --green: #00a229;
--text: white; --text: white;
--foreground: #0f0f0f; --foreground: #0f0f0f;

View File

@ -1,6 +1,6 @@
/* Dark theme setting */ /* Dark theme setting */
.dark{ .dark{
--accent: aqua; --accent: #d54455;
--green: #5cff85; --green: #5cff85;
--text: white; --text: white;
--foreground: #222; --foreground: #222;

View File

@ -0,0 +1,14 @@
/* Libreddit black theme setting */
.libredditBlack {
--accent: #009a9a;
--green: #00a229;
--text: white;
--foreground: #0f0f0f;
--background: black;
--outside: black;
--post: black;
--panel-border: 2px solid #0f0f0f;
--highlighted: #0f0f0f;
--visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

View File

@ -0,0 +1,14 @@
/* Libreddit dark theme setting */
.libredditDark{
--accent: aqua;
--green: #5cff85;
--text: white;
--foreground: #222;
--background: #0f0f0f;
--outside: #1f1f1f;
--post: #161616;
--panel-border: 1px solid #333;
--highlighted: #333;
--visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

View File

@ -0,0 +1,19 @@
/* Libreddit light theme setting */
.libredditLight {
--accent: #009a9a;
--green: #00a229;
--text: black;
--foreground: #f5f5f5;
--background: #ddd;
--outside: #ececec;
--post: #eee;
--panel-border: 1px solid #ccc;
--highlighted: white;
--visited: #555;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
html:has(> .libredditLight) {
/* Hint color theme to browser for scrollbar */
color-scheme: light;
}

View File

@ -1,6 +1,6 @@
/* Light theme setting */ /* Light theme setting */
.light { .light {
--accent: #009a9a; --accent: #bb2b3b;
--green: #00a229; --green: #00a229;
--text: black; --text: black;
--foreground: #f5f5f5; --foreground: #f5f5f5;

View File

@ -8,6 +8,9 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <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="description" content="View on Redlib, an alternative private front-end to Reddit.">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 --> <!-- General PWA -->
<meta name="theme-color" content="#1F1F1F"> <meta name="theme-color" content="#1F1F1F">
<!-- iOS Application --> <!-- iOS Application -->
@ -35,7 +38,7 @@
<nav class=" <nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}"> {% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<div id="logo"> <div id="logo">
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a> <a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib.</span></a>
{% block subscriptions %}{% endblock %} {% block subscriptions %}{% endblock %}
</div> </div>
{% block search %}{% endblock %} {% block search %}{% endblock %}

View File

@ -24,7 +24,7 @@
{% if author.flair.flair_parts.len() > 0 %} {% if author.flair.flair_parts.len() > 0 %}
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small> <small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
{% endif %} {% 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 edited.0 != "".to_string() %}<span class="edited" title="{{ edited.1 }}">edited {{ edited.0 }}</span>{% endif %}
{% if !awards.is_empty() && prefs.hide_awards != "on" %} {% if !awards.is_empty() && prefs.hide_awards != "on" %}
<span class="dot">&bull;</span> <span class="dot">&bull;</span>

View File

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

View File

@ -3,6 +3,10 @@
{% block title %}Redlib Settings{% endblock %} {% block title %}Redlib Settings{% endblock %}
{% block subscriptions %}
{% call utils::sub_list("") %}
{% endblock %}
{% block search %} {% block search %}
{% call utils::search("".to_owned(), "") %} {% call utils::search("".to_owned(), "") %}
{% endblock %} {% endblock %}
@ -46,6 +50,21 @@
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}> <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 %} {% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
</div> </div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Content</legend> <legend>Content</legend>
@ -78,21 +97,24 @@
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}> <input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
</div> </div>
{% endif %} {% endif %}
<div class="prefs-group">
<label for="hide_awards">Hide awards</label>
<input type="hidden" value="off" name="hide_awards">
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_score">Hide score</label>
<input type="hidden" value="off" name="hide_score">
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
</div>
</fieldset>
<fieldset>
<legend>Media</legend>
<div class="prefs-group"> <div class="prefs-group">
<label for="autoplay_videos">Autoplay videos</label> <label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos"> <input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}> <input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group"> <div class="prefs-group">
<label for="use_hls">Use HLS for videos</label> <label for="use_hls">Use HLS for videos</label>
{% if prefs.ffmpeg_video_downloads != "on" %} {% if prefs.ffmpeg_video_downloads != "on" %}
@ -119,21 +141,6 @@
<input type="hidden" value="off" name="hide_hls_notification"> <input type="hidden" value="off" name="hide_hls_notification">
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}> <input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group">
<label for="hide_awards">Hide awards</label>
<input type="hidden" value="off" name="hide_awards">
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_score">Hide score</label>
<input type="hidden" value="off" name="hide_score">
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset> </fieldset>
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>

View File

@ -27,7 +27,7 @@
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %} {% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
{% endif %} {% endif %}
</div> </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") %} {% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
</select> </select>
<button id="sort_submit" class="submit"> <button id="sort_submit" class="submit">
@ -134,7 +134,13 @@
</form> </form>
{% endif %} {% endif %}
</div> </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> </div>
</details> </details>
<details class="panel" id="sidebar" open> <details class="panel" id="sidebar" open>

View File

@ -1,78 +1,87 @@
{% extends "base.html" %} {% extends "base.html" %} {% import "utils.html" as utils %} {% block search %}
{% import "utils.html" as utils %} {% 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" />
&rarr;
</svg>
</button>
</form>
{% block search %} {% if all_posts_hidden_nsfw %}
{% call utils::search("".to_owned(), "") %} <center>
{% endblock %} All posts are hidden because they are NSFW. Enable "Show NSFW posts"
in settings to view.
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} </center>
{% endif %} {% if no_posts %}
{% block subscriptions %} <center>No posts were found.</center>
{% call utils::sub_list("") %} {% endif %} {% if all_posts_filtered %}
{% endblock %} <center>(All content on this page has been filtered)</center>
{% else %}
{% block body %} <div id="posts">
<main> {% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
{% if !is_filtered %} "on" %} {% else if !post.title.is_empty() %} {% call
<div id="column_one"> utils::post_in_list(post) %} {% else %}
<form id="sort"> <div class="comment user-comment">
<div id="listing_options"> <div class="comment_left">
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %} <p class="comment_score" title="{{ post.score.1 }}">
</div> {% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
<select id="sort_select" name="sort"> else %} &#x2022; {% endif %}
{% 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" />
&rarr;
</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 %}
&#x2022;
{% endif %}
</p> </p>
<div class="line"></div> <div class="line"></div>
</div> </div>
<details class="comment_right" open> <details class="comment_right" open>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a> <a
<span class="created">&nbsp;in&nbsp;</span> class="comment_link"
<a href="/r/{{ post.community }}">r/{{ post.community }}</a> href="{{ post.permalink }}#{{ post.id }}"
<span class="created" title="{{ post.created }}">&nbsp;{{ post.rel_time }}</span> title="{{ post.link_title }}"
</summary> >{{ post.link_title }}</a
<p class="comment_body">{{ post.body|safe }}</p> >
</details> <div class="user_comment_data_divider">
</div> <span class="created-in">&nbsp;in&nbsp;</span>
{% endif %} <a
{% endfor %} class="comment_subreddit"
href="/r/{{ post.community }}"
>r/{{ post.community }}</a
>
<span class="dot">&bull;</span>
<span class="created" title="{{ post.created }}"
>&nbsp;{{ post.rel_time }}</span
>
</div>
</summary>
<p class="comment_body">{{ post.body|safe }}</p>
</details>
</div>
{% endif %} {% endfor %}
{% if prefs.ffmpeg_video_downloads == "on" %} {% if prefs.ffmpeg_video_downloads == "on" %}
<script src="/ffmpeg/ffmpeg.js"></script> <script src="/ffmpeg/ffmpeg.js"></script>
<script src="/ffmpeg/ffmpeg-util.js"></script> <script src="/ffmpeg/ffmpeg-util.js"></script>
@ -81,61 +90,94 @@
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<script src="/videoUtils.js"></script> <script src="/videoUtils.js"></script>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<footer> <footer>
{% if ends.0 != "" %} {% if ends.0 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a> <a
{% endif %} href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}"
accesskey="P"
{% if ends.1 != "" %} >PREV</a
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a> >
{% endif %} {% endif %} {% if ends.1 != "" %}
</footer> <a
</div> href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
{% endif %} accesskey="N"
<aside> >NEXT</a
{% if is_filtered %} >
<center>(Content from u/{{ user.name }} has been filtered)</center> {% endif %}
{% endif %} </footer>
<div class="panel" id="user"> </div>
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon"> {% endif %}
<h1 id="user_title">{{ user.title }}</h1> <aside>
<p id="user_name">u/{{ user.name }}</p> {% if is_filtered %}
<div id="user_description">{{ user.description }}</div> <center>(Content from u/{{ user.name }} has been filtered)</center>
<div id="user_details"> {% endif %}
<label>Karma</label> <div class="panel" id="user">
<label>Created</label> <img
<div>{{ user.karma }}</div> loading="lazy"
<div>{{ user.created }}</div> id="user_icon"
</div> src="{{ user.icon }}"
<div id="user_actions"> alt="User icon"
{% let name = ["u_", user.name.as_str()].join("") %} />
<div id="user_subscription"> <h1 id="user_title">{{ user.title }}</h1>
{% if prefs.subscriptions.contains(name) %} <p id="user_name">u/{{ user.name }}</p>
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST"> <div id="user_description">{{ user.description }}</div>
<button class="unsubscribe">Unfollow</button> <div id="user_details">
</form> <label>Karma</label>
{% else %} <label>Created</label>
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST"> <div>{{ user.karma }}</div>
<button class="subscribe">Follow</button> <div>{{ user.created }}</div>
</form> </div>
{% endif %} <div id="user_actions">
</div> {% let name = ["u_", user.name.as_str()].join("") %}
<div id="user_filter"> <div id="user_subscription">
{% if prefs.filters.contains(name) %} {% if prefs.subscriptions.contains(name) %}
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST"> <form
<button class="unfilter">Unfilter</button> action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
</form> method="POST"
{% else %} >
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST"> <button class="unsubscribe">Unfollow</button>
<button class="filter">Filter</button> </form>
</form> {% else %}
{% endif %} <form
</div> action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
</div> method="POST"
</div> >
</aside> <button class="subscribe">Follow</button>
</main> </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 %} {% endblock %}

View File

@ -64,7 +64,7 @@
{% macro post(post) -%} {% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%} {% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT --> <!-- POST CONTENT -->
<div class="post highlighted"> <div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
<p class="post_header"> <p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
@ -87,12 +87,12 @@
{% endif %} {% endif %}
</p> </p>
<h1 class="post_title"> <h1 class="post_title">
{{ post.title }}
{% if post.flair.flair_parts.len() > 0 %} {% 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" <a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
class="post_flair" class="post_flair"
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a> style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
{{ post.title }}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} {% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %} {% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h1> </h1>
@ -104,12 +104,11 @@
<a href="{{ post.media.url }}" class="post_media_image" > <a href="{{ post.media.url }}" class="post_media_image" >
{% if post.media.height == 0 || post.media.width == 0 %} {% if post.media.height == 0 || post.media.width == 0 %}
<!-- i.redd.it images special case --> <!-- i.redd.it images special case -->
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"{%if post_should_be_blurred %} class="post_nsfw_blur"{% endif %}/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.media.url }}"/> <image width="100%" height="100%" href="{{ post.media.url }}"/>
<desc> <desc>
@ -127,7 +126,7 @@
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls> <video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
@ -135,7 +134,7 @@
<script src="/videoUtils.js"></script> <script src="/videoUtils.js"></script>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(post.permalink[1..]) %} {% call render_hls_notification(post.permalink[1..]) %}
{% endif %} {% endif %}
@ -158,7 +157,10 @@
{% endif %} {% endif %}
<!-- POST BODY --> <!-- POST BODY -->
<div class="post_body">{{ post.body|safe }}</div> <div class="post_body">
{{ post.body|safe }}
{% call poll(post) %}
</div>
<div class="post_score" title="{{ post.score.1 }}"> <div class="post_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %} {% if prefs.hide_score != "on" %}
{{ post.score.0 }} {{ post.score.0 }}
@ -180,6 +182,10 @@
</a> </a>
</li> </li>
{% endif %} {% 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) %} {% call external_reddit_link(post.permalink) %}
{% if post.media.download_name != "" %} {% if post.media.download_name != "" %}
@ -215,7 +221,7 @@
{% macro post_in_list(post) -%} {% macro post_in_list(post) -%}
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%} {% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}"> <div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
<p class="post_header"> <p class="post_header">
{% let community -%} {% let community -%}
{% if post.community.starts_with("u_") -%} {% if post.community.starts_with("u_") -%}
@ -254,7 +260,6 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -269,19 +274,19 @@
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || 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() %} {% 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"> <div class="post_media_content">
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none"> <video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %} {% endif %}
{% else if post.post_type != "self" %} {% else if post.post_type != "self" %}
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow"> <a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
{% if post.thumbnail.url.is_empty() %} {% if post.thumbnail.url.is_empty() %}
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
<title>Thumbnail</title> <title>Thumbnail</title>
@ -289,7 +294,7 @@
</svg> </svg>
{% else %} {% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;"> <div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post_should_be_blurred %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg"> <svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/> <image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc> <desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/> <img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>