Compare commits

...

90 Commits

Author SHA1 Message Date
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
Matthew Esposito
793047f63f fix(client): revert to hyper-rustls=0.24.2 2024-09-18 11:24:00 -04:00
Matthew Esposito
3625fdfdbe fix(ci): temporarily disable README updates 2024-09-17 14:42:09 -04:00
Matthew Esposito
28f85f2599 Attempting to fix main-docker.yml for downloading digests 2024-09-17 14:39:42 -04:00
wuchyi
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
wuchyi
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
Matthew Esposito
8ef45456d6 actions trigger 2024-09-16 16:39:07 -04:00
Matthew Esposito
118708400a fix(ci): unique name 2024-09-16 16:36:24 -04:00
Matthew Esposito
28e72c9058
fix(ci): bump versions (#234) 2024-09-16 16:29:52 -04:00
Matthew Esposito
0b15250cc8
fix(oauth): catch network policy violation and rate limit (#233) 2024-09-16 16:16:08 -04:00
Matthew Esposito
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
Butter Cat
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
Kot C
c494fbec31
Insert noindex meta for ROBOTS_DISABLE_INDEXING (#199) (#207) 2024-09-03 19:44:04 -04:00
Butter Cat
438e412be3
Add anchor to comment link (#212) 2024-09-03 19:21:33 -04:00
dependabot[bot]
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
b076b076e3 v0.35.2 2024-08-31 10:20:57 +12:00
fdaba8344d set video download MIME
fixes #11
2024-08-31 10:18:05 +12:00
Davide Cavalca
041ecceeaf
Update license tag (#200) 2024-08-07 14:09:54 -04:00
Butter Cat
3677ca10e2
Fix sort options overflow on small screens (#192) 2024-07-25 20:26:27 -04:00
Butter Cat
21fd34710c
Fix font sizes in search bar being incorrect (#190) 2024-07-24 19:56:06 -04:00
Jere Vuola
4205959ade
fix(docs): Add env HIDE_SIDEBAR_AND_SUMMARY (#188) 2024-07-23 21:02:15 -04:00
Matthew Esposito
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
Matthew Esposito
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
syeopite
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
syeopite
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
eedbe06af6 Merge branch 'ugly-layouts'
sure why not :)
- last words 2024
2024-07-07 19:34:27 +12:00
6042bca612 mosaic -> waterfall 2024-07-07 18:35:53 +12:00
7e85ae25b1 Merge remote-tracking branch 'upstream/main'
+ repository link update
2024-07-07 17:19:28 +12:00
Pim
4f21388643
fix: also use hls if possible for gifs in post_in_list macro (#177) 2024-07-05 16:33:06 -04:00
Pim
8a917fcde3
feat: add download button on image/gif/video posts (#173)
* feat: add download button on image/gif/video posts

* chore: fix formatting

* chore: dont create reference
2024-07-04 21:32:12 -04:00
Matthew Esposito
67a890cab3
fix(posts): fix sort call on new (#171) 2024-07-02 08:04:27 -04:00
Pim
366bc17f97
feat: show post link title for comments on user page (#169) 2024-07-01 17:15:50 -04:00
Matthew Esposito
d9e7681004 v0.35.1 2024-06-29 13:28:18 -04:00
Matthew Esposito
f74d1affb6
fix(posts): manually sort by flags (#168)
* fix(posts): manually sort by flags

* fix(posts): shorten sort call
2024-06-29 13:26:09 -04:00
Matthew Esposito
f44638a2cb v0.35.0 2024-06-29 12:00:34 -04:00
Matthew Esposito
beb4cf193b
fix(posts): manually sort by created date (#166) 2024-06-29 11:48:42 -04:00
Matthew Esposito
c565ebfb01 refactor(log): update some logs 2024-06-29 10:44:33 -04:00
2b8112a3fb mosaic layout 2024-06-30 00:04:46 +12:00
Matthew Esposito
459a8e1245 refactor(log): shorten some logs 2024-06-29 00:20:19 -04:00
Matthew Esposito
0f7eba717e fix(client): Handle invalid reddit response of base URL location 2024-06-28 22:41:36 -04:00
Matthew Esposito
ea87ec33a1
fix(subreddit): handle plus-encoding errors even better (#163)
* fix(subreddit): handle plus-encoding errors even better

* chore(clippy): fix lint
2024-06-28 22:28:58 -04:00
Matthew Esposito
102cd2f23f
Merge pull request #162 from redlib-org/oauth_arc_swap
fix(oauth): arc_swap
2024-06-28 18:17:00 -04:00
Matthew Esposito
3b2ad212d5 fix(oauth): arc_swap 2024-06-28 18:14:47 -04:00
Matthew Esposito
4dc7ff8165
Merge pull request #160 from redlib-org/oauth_oppenheimer
fix(oauth): even more atomics to avoid simultaneous token rollover
2024-06-27 23:35:51 -04:00
Matthew Esposito
2f8a38d8c7 chore(clippy): fix lint 2024-06-27 23:34:27 -04:00
Matthew Esposito
13083e999c fix(oauth): handle extremely rare race condition by atomically compare_exchanging 2024-06-27 23:32:17 -04:00
Matthew Esposito
4e2ec3fbc9 fix(oauth): handle case where a rate limit sneaks in 2024-06-27 23:29:55 -04:00
Matthew Esposito
89313f73e6 fix(oauth): atomics to avoid simultaneous token rollover 2024-06-27 23:26:31 -04:00
1644e68e43 v0.35.1 2024-06-28 13:34:12 +12:00
f5d4cc49fb fix some default options #7 2024-06-28 13:33:14 +12:00
f22f7841a8 Merge remote-tracking branch 'upstream/main' 2024-06-28 12:40:15 +12:00
b6c6e64cfe move the restore note
my scroll wheel will fall off if i have to scroll to get down there ohmagaw
2024-06-28 12:31:31 +12:00
6d3db31b11 Merge pull request 'README updates' (#9) from README-update into main
Reviewed-on: #9
2024-06-28 00:34:03 +12:00
f0eb496d6a fix README.md 2024-06-28 00:32:20 +12:00
e984646ae6 Update README.md
add information about the name, a thank you and bits here and there
2024-06-28 00:07:29 +12:00
60c0d63583 nsfw_landing style & function changes
theme the NSFW title and allow the user to directly change their setting
2024-06-27 17:43:43 +12:00
Matthew Esposito
3bd8b511a7 fix(oauth): strengthen sync guarantees 2024-06-26 23:41:26 -04:00
Matthew Esposito
8c5aaaa33d feat(scripts): add load testing 2024-06-26 23:40:31 -04:00
bd0a312487 add disclaimer 2024-06-27 15:04:58 +12:00
Matthew Esposito
023cc8505b
Merge pull request #158 from redlib-org/oauth_proper_atomics
fix(oauth): reset rate limit earlier in refresh cycle
2024-06-26 22:20:00 -04:00
Matthew Esposito
2e476dea63 fix(oauth): reset rate limit earlier in refresh cycle 2024-06-26 22:16:41 -04:00
f398d16c22 fix ffmpeg_video_downloads on cookie restore 2024-06-27 14:13:16 +12:00
Matthew Esposito
d045a5760a
Merge pull request #156 from redlib-org/fix_oauth_ratelimit
feat(oauth): roll over oauth key on rate limit
2024-06-26 19:33:04 -04:00
Matthew Esposito
07bf20dbc0 feat(oauth): roll over oauth key on rate limit 2024-06-26 19:19:30 -04:00
Matthew Esposito
518bf03e04 fix(client): Add trace logging for ratelimit info, render error page if exceeded 2024-06-26 08:05:22 -04:00
Matthew Esposito
00dee52320 chore(deps): Cargo update 2024-06-25 20:42:06 -04:00
Matthew Esposito
48873c01b9
Merge pull request #154 from redlib-org/fix_multi_sub
fix(subreddit): handle plus-encoding errors
2024-06-25 19:52:33 -04:00
Matthew Esposito
951fe400ae fix(subreddit): handle plus-encoding errors 2024-06-25 19:50:00 -04:00
Matthew Esposito
bacc9e35df refactor(oauth): leave android header unmodified (fixes #131) 2024-06-25 19:28:41 -04:00
Matthew Esposito
724b960112
Merge pull request #149 from pimlie/feat-blur-spoiler-previews
feat: add support to blur spoiler previews
2024-06-23 10:28:46 -04:00
pimlie
69f9d9ff3c feat: also blur spoiler previews on post view 2024-06-22 12:47:33 +02:00
pimlie
1d44bd180e feat: add spoiler badge to post title 2024-06-22 12:38:13 +02:00
pimlie
3301da1ef1 feat: add support to blur spoiler previews 2024-06-22 12:16:12 +02:00
138934f30c change resource links and container links 2024-06-21 23:49:37 +12:00
5396c0783e add mascots to user settings readme 2024-06-21 23:11:23 +12:00
Matthew Esposito
213481ef53
Merge pull request #148 from Pix3l01/fix-healthcheck
Make server listen on both IPv6 and IPv4 by default to fix docker healthcheck
2024-06-20 08:06:27 -04:00
Matthew Esposito
2d5cfcb41d
Merge pull request #146 from ac615223s5/default-filter
Default filters
2024-06-20 07:58:41 -04:00
Matthew Esposito
f460895cc0 chore(clippy): fix lint 2024-06-20 07:56:43 -04:00
ac615223s5
5193164719
Merge branch 'redlib-org:main' into default-filter 2024-06-19 20:10:05 -04:00
Matthew Esposito
9013e589dd chore(clippy): fix lint 2024-06-19 14:45:55 -04:00
Matthew Esposito
997cd8f829 feat(bug): Improve bug reporting while keeping logs private 2024-06-19 14:45:32 -04:00
Matthew Esposito
5a13b9892b chore(clippy): add lint 2024-06-19 14:28:48 -04:00
Alessandro Pizzorni
91975865b8 Make server listen on both IPv6 and IPv4 by default 2024-06-19 00:42:38 +02:00
nieve
3491e754ac Update .gitignore 2024-06-18 15:16:41 -04:00
nieve
1408c32a4d Update .env.example 2024-06-18 00:25:29 -04:00
nieve
30944579d7 add default filter config 2024-06-18 00:21:00 -04:00
32 changed files with 2535 additions and 1482 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target

View File

@ -26,18 +26,24 @@ REDLIB_DEFAULT_WIDE=off
REDLIB_DEFAULT_POST_SORT=hot REDLIB_DEFAULT_POST_SORT=hot
# Set the default comment sort method (options: confidence, top, new, controversial, old) # Set the default comment sort method (options: confidence, top, new, controversial, old)
REDLIB_DEFAULT_COMMENT_SORT=confidence REDLIB_DEFAULT_COMMENT_SORT=confidence
# Enable blurring Spoiler content by default
REDLIB_DEFAULT_BLUR_SPOILER=off
# Enable showing NSFW content by default # Enable showing NSFW content by default
REDLIB_DEFAULT_SHOW_NSFW=off REDLIB_DEFAULT_SHOW_NSFW=off
# Enable blurring NSFW content by default # Enable blurring NSFW content by default
REDLIB_DEFAULT_BLUR_NSFW=off REDLIB_DEFAULT_BLUR_NSFW=off
# Enable HLS video format by default # Enable HLS video format by default
REDLIB_DEFAULT_USE_HLS=off REDLIB_DEFAULT_USE_HLS=off
# Enable audio+video downloads with ffmpeg.wasm
REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
# Hide HLS notification by default # Hide HLS notification by default
REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
# Disable autoplay videos by default # Disable autoplay videos by default
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3) # Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
REDLIB_DEFAULT_SUBSCRIPTIONS= REDLIB_DEFAULT_SUBSCRIPTIONS=
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
REDLIB_DEFAULT_FILTERS=
# Hide awards by default # Hide awards by default
REDLIB_DEFAULT_HIDE_AWARDS=off REDLIB_DEFAULT_HIDE_AWARDS=off
# Hide sidebar and summary # Hide sidebar and summary

View File

@ -19,11 +19,9 @@ jobs:
- { 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

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/target /target
.env .env
redlib.toml
# Idea Files # Idea Files
.idea/ .idea/

View File

@ -1,2 +0,0 @@
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-musl/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
language = "bash"

419
Cargo.lock generated
View File

@ -71,6 +71,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]] [[package]]
name = "askama" name = "askama"
version = "0.12.1" version = "0.12.1"
@ -92,7 +98,7 @@ dependencies = [
"mime_guess", "mime_guess",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -118,7 +124,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "syn 2.0.68",
]
[[package]]
name = "atom_syndication"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
dependencies = [
"chrono",
"derive_builder 0.20.0",
"diligent-date-parser",
"never",
"quick-xml 0.31.0",
] ]
[[package]] [[package]]
@ -129,9 +148,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.72" version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
@ -142,6 +161,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"
@ -150,9 +175,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -208,9 +233,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]] [[package]]
name = "cached" name = "cached"
version = "0.51.3" version = "0.51.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd93a9f06ec296ca66b4c26fafa9ed63f32c473d7a708a5f28563ee64c948515" checksum = "0feb64151eed3da6107fddd2d717a6ca4b9dbd74e43784c55c841d1abfe5a295"
dependencies = [ dependencies = [
"ahash", "ahash",
"async-trait", "async-trait",
@ -230,10 +255,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 2.0.66", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -244,9 +269,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.98" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -255,19 +280,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "chrono"
version = "4.5.4" version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]]
name = "clap"
version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.2" version = "4.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -275,9 +309,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]] [[package]]
name = "cookie" name = "cookie"
@ -342,14 +376,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]]
@ -362,8 +420,19 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim 0.11.1",
"syn 2.0.66", "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]]
@ -372,9 +441,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 2.0.66", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -392,6 +461,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"
@ -402,12 +533,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"
@ -637,9 +786,9 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
@ -655,9 +804,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.28" version = "0.14.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -679,9 +828,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",
@ -689,7 +838,6 @@ dependencies = [
"log", "log",
"rustls", "rustls",
"rustls-native-certs", "rustls-native-certs",
"rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
] ]
@ -810,9 +958,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.2" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "mime" name = "mime"
@ -838,9 +986,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.3" version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [ dependencies = [
"adler", "adler",
] ]
@ -856,6 +1004,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"
@ -872,6 +1026,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"
@ -893,9 +1056,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.35.0" version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -983,9 +1146,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.84" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -996,6 +1159,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"
@ -1041,10 +1224,11 @@ dependencies = [
[[package]] [[package]]
name = "redsunlib" name = "redsunlib"
version = "0.35.0" version = "0.35.2"
dependencies = [ dependencies = [
"arc-swap",
"askama", "askama",
"base64", "base64 0.22.1",
"brotli", "brotli",
"build_html", "build_html",
"cached", "cached",
@ -1063,6 +1247,7 @@ dependencies = [
"pretty_env_logger", "pretty_env_logger",
"regex", "regex",
"route-recognizer", "route-recognizer",
"rss",
"rust-embed", "rust-embed",
"sealed_test", "sealed_test",
"serde", "serde",
@ -1077,9 +1262,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.4" version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1089,9 +1274,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.6" version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1100,9 +1285,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]] [[package]]
name = "ring" name = "ring"
@ -1131,6 +1316,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"
@ -1151,7 +1348,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.66", "syn 2.0.68",
"walkdir", "walkdir",
] ]
@ -1187,55 +1384,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",
] ]
@ -1282,10 +1468,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sealed_test" name = "sct"
version = "1.0.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sealed_test"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1867f8f005bd7fb73c367e2e45dd628417906a2ca27597fe59cbf04279a222"
dependencies = [ dependencies = [
"fs_extra", "fs_extra",
"rusty-forkfork", "rusty-forkfork",
@ -1295,12 +1491,12 @@ dependencies = [
[[package]] [[package]]
name = "sealed_test_derive" name = "sealed_test_derive"
version = "1.0.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697" checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
dependencies = [ dependencies = [
"quote", "quote",
"syn 1.0.109", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1343,14 +1539,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "syn 2.0.68",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.117" version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1430,18 +1626,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 = "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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -1455,9 +1651,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.66" version = "2.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1502,7 +1698,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "syn 2.0.68",
] ]
[[package]] [[package]]
@ -1540,9 +1736,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -1555,9 +1751,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.37.0" version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -1574,23 +1770,22 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "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",
] ]
@ -1609,9 +1804,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.13" version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -1630,9 +1825,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.13" version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -1722,9 +1917,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@ -1733,9 +1928,9 @@ dependencies = [
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.8.0" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]
@ -1930,9 +2125,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.9" version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -1954,11 +2149,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.66", "syn 2.0.68",
] ]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

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/redlib" repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.0" version = "0.35.2"
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>",
@ -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"
@ -42,6 +42,9 @@ 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"
[dev-dependencies] [dev-dependencies]
lipsum = "0.9.0" lipsum = "0.9.0"

130
README.md
View File

@ -1,37 +1,45 @@
<img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo"> <img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo">
# Redsunlib # Redsunlib
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some function and cosmetic changes. > An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some <sup><sub>(minor)</sub></sup> function and cosmetic changes.
<br> <br>
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20) ![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
## Table of Contents ### Disclaimer
1. [Redlib](#redlib) There are rapid changes/features in this fork that can<sup>(will)</sup> change without notice. If you want to host this version, be aware that it's likely to break at some point. I still wouldn't recommend it in a production environment unless you know what you're doing. Or like living on the edge.......
2. [Instances](#instances)
3. [About](#about) > I would also like to thank the maintainers and contributors of both [Redlib](https://github.com/redlib-org/redlib) and [Libreddit](https://github.com/libreddit/libreddit) for all the work they did while I just added some low quality tacky features. ❤️
---
## Table of Contents
1. [Redsunlib](#redsunlib)
- [Disclaimer](#disclaimer)
2. [Table of Contents](#table-of-contents)
3. [Instances](#instances)
4. [About](#about)
- [The Name](#the-name)
- [Built with](#built-with) - [Built with](#built-with)
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends) - [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
- [Teddit](#teddit) - [Teddit](#teddit)
- [Libreddit](#libreddit) - [Libreddit](#libreddit)
4. [Comparison](#comparison) 5. [Comparison](#comparison)
- [Speed](#speed) - [Speed](#speed)
- [Privacy](#privacy) - [Privacy](#privacy)
- [Reddit](#reddit) - [Reddit](#reddit)
- [Redlib](#redlib-1) - [Redlib](#redlib-1)
- [Server](#server) - [Server](#server)
5. [Deployment](#deployment) 6. [Deployment](#deployment)
- [Docker](#docker) - [Docker](#docker)
- [Docker Compose](#docker-compose) - [Docker Compose](#docker-compose)
- [Docker CLI](#docker-cli) - [Docker CLI](#docker-cli)
- [Binary](#binary) - [Binary](#binary)
- [Running as a systemd service](#running-as-a-systemd-service) - [Running as a systemd service](#running-as-a-systemd-service)
- [Building from source](#building-from-source) - [Building from source](#building-from-source)
- [Replit/Heroku/Glitch](#replit-heroku-glitch) 7. [Configuration](#configuration)
- [launchd (macOS)](#launchd-macos)
6. [Configuration](#configuration)
- [Instance settings](#instance-settings) - [Instance settings](#instance-settings)
- [Default user settings](#default-user-settings) - [Default user settings](#default-user-settings)
@ -39,14 +47,10 @@
# Instances # Instances
> [!TIP] > [!WARNING]
> 🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!** > 🔗 **Currently public Redsunlib instance are not available, consider using a [redlib](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) instance if you are not comfortable running your own**
An up-to-date table of instances is available in [Markdown](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) and [machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json). You are more than welcome to host an instance and submit an issue if you want it added. That is, if you've read the [Disclaimer](#disclaimer) and it's within your "personal risk tolerance." ;)
Both files are part of the [redlib-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [redlib-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
For information on instance uptime, see the [Uptime Robot status page](https://stats.uptimerobot.com/mpmqAs1G2Q).
--- ---
@ -56,6 +60,13 @@ Redlib hopes to provide an easier way to browse Reddit, without the ads, tracker
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues). Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
## The Name
**Red sun** in the sky + Red**lib** = Redsunlib
And at the time, I was reading an excerpt from Mao Zedong, so the name seemed appropriate. But paradoxically named since Reddit is basically the sinophobia capital of the internet :/
## Built with ## Built with
- [Rust](https://www.rust-lang.org/) - Programming language - [Rust](https://www.rust-lang.org/) - Programming language
@ -158,7 +169,7 @@ For configuration options, see the [Configuration section](#Configuration).
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host. [Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
Docker images for Redlib are available at [quay.io](https://quay.io/repository/redlib/redlib), with support for `amd64`, `arm64`, and `armv7` platforms. Docker images for Redsunlib are available at our [Gitea container registry](https://git.stardust.wtf/iridium/-/packages/container/redsunlib/latest), currently only with support for `amd64`, if you need `arm64`, or `armv7` platforms you can either build Redsunlib yourself or open an [issue](https://git.stardust.wtf/iridium/redsunlib/issues) :)
### Docker Compose ### Docker Compose
@ -184,15 +195,15 @@ docker logs -f redlib
Deploy Redlib: Deploy Redlib:
```bash ```bash
docker pull quay.io/redlib/redlib:latest docker pull git.stardust.wtf/iridium/redsunlib:latest
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib:latest docker run -d --name redlib -p 8080:8080 git.stardust.wtf/iridium/redsunlib:latest
``` ```
Deploy using a different port on the host (in this case, port 80): Deploy using a different port on the host (in this case, port 80):
```bash ```bash
docker pull quay.io/redlib/redlib:latest docker pull git.stardust.wtf/iridium/redsunlib:latest
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib:latest docker run -d --name redlib -p 80:8080 git.stardust.wtf/iridium/redsunlib:latest
``` ```
If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`. If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`.
@ -205,19 +216,7 @@ docker logs -f redlib
## Binary ## Binary
If you're on Linux, you can grab a binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest) from GitHub. Currently binaries are not supplied at this moment but will be at some point in the future but can be [built from source](#building-from-source)
Download the binary using [Wget](https://www.gnu.org/software/wget/):
```bash
wget https://github.com/redlib-org/redlib/releases/download/v0.31.0/redlib
```
Make the binary executable and change its ownership to `root`:
```bash
sudo chmod +x redlib && sudo chown root:root redlib
```
Copy the binary to `/usr/bin`: Copy the binary to `/usr/bin`:
@ -261,59 +260,13 @@ Before=nginx.service
## Building from source ## Building from source
To deploy Redlib with changes not yet included in the latest release, you can build the application from source. To deploy Redsunlib with changes not yet included in the latest release, you can build the application from source.
```bash ```bash
git clone https://github.com/redlib-org/redlib && cd redlib git clone https://git.stardust.wtf/iridium/redsunlib && cd redsunlib
cargo run cargo run
``` ```
## Replit/Heroku
> [!WARNING]
> These are free hosting options, but they are _not_ private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
## launchd (macOS)
If you are on macOS, you can use the [launchd](https://en.wikipedia.org/wiki/Launchd) service available in `contrib/redlib.plist`.
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`.
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
<!-- ## Cargo
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
```bash
cargo install libreddit
``` -->
<!-- ## AUR
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
```bash
yay -S libreddit-git
```
## NetBSD/pkgsrc
For NetBSD users, Redlib is available from the official repositories.
```bash
pkgin install libreddit
```
Or, if you prefer to build from source
```bash
cd /usr/pkgsrc/libreddit
make install
``` -->
--- ---
# Configuration # Configuration
@ -343,7 +296,7 @@ REDLIB_DEFAULT_USE_HLS = "on"
> If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example: > If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example:
> >
> ```bash > ```bash
> docker run -d --name redlib -p 8080:8080 --env-file .env quay.io/redlib/redlib:latest > docker run -d --name redlib -p 8080:8080 --env-file .env git.stardust.wtf/iridium/redsunlib:latest
> ``` > ```
> >
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line. > If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
@ -359,7 +312,8 @@ Assign a default value for each instance-specific setting by passing environment
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. | | `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.
@ -367,18 +321,22 @@ Assign a default value for each user-modifiable setting by passing environment v
| Name | Possible values | Default value | | Name | Possible values | Default value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` | | `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` | | `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` | | `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` | | `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` | | `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` | | `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
| `SHOW_NSFW` | `["on", "off"]` | `off` | | `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` | | `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` | | `USE_HLS` | `["on", "off"]` | `off` |
| `FFMPEG_VIDEO_DOWNLOADS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` | | `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` | | `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ | | `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
| `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

@ -1,5 +1,5 @@
{ {
"name": "Redlib", "name": "Redsunlib",
"description": "Private front-end for Reddit", "description": "Private front-end for Reddit",
"buildpacks": [ "buildpacks": [
{ {
@ -14,6 +14,9 @@
"REDLIB_DEFAULT_THEME": { "REDLIB_DEFAULT_THEME": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_MASCOT": {
"required": false
},
"REDLIB_DEFAULT_FRONT_PAGE": { "REDLIB_DEFAULT_FRONT_PAGE": {
"required": false "required": false
}, },
@ -29,13 +32,19 @@
"REDLIB_DEFAULT_POST_SORT": { "REDLIB_DEFAULT_POST_SORT": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_BLUR_SPOILER": {
"required": false
},
"REDLIB_DEFAULT_SHOW_NSFW": { "REDLIB_DEFAULT_SHOW_NSFW": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_BLUR_NSFW": { "REDLIB_DEFAULT_BLUR_NSFW": {
"required": false "required": false
}, },
"REDLIB_USE_HLS": { "REDLIB_DEFAULT_USE_HLS": {
"required": false
},
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS": {
"required": false "required": false
}, },
"REDLIB_HIDE_HLS_NOTIFICATION": { "REDLIB_HIDE_HLS_NOTIFICATION": {
@ -59,11 +68,20 @@
"REDLIB_DEFAULT_SUBSCRIPTIONS": { "REDLIB_DEFAULT_SUBSCRIPTIONS": {
"required": false "required": false
}, },
"REDLIB_DEFAULT_FILTERS": {
"required": false
},
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": { "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
"required": false "required": false
}, },
"REDLIB_PUSHSHIFT_FRONTEND": { "REDLIB_PUSHSHIFT_FRONTEND": {
"required": false "required": false
},
"REDLIB_ENABLE_RSS": {
"required": false
},
"REDLIB_FULL_URL": {
"required": false
} }
} }
} }

View File

@ -1,14 +1,17 @@
ADDRESS=0.0.0.0 ADDRESS=0.0.0.0
PORT=12345 PORT=12345
#REDLIB_DEFAULT_THEME=default #REDLIB_DEFAULT_THEME=default
#REDLIB_DEFAULT_MASCOT=none
#REDLIB_DEFAULT_FRONT_PAGE=default #REDLIB_DEFAULT_FRONT_PAGE=default
#REDLIB_DEFAULT_LAYOUT=card #REDLIB_DEFAULT_LAYOUT=card
#REDLIB_DEFAULT_WIDE=off #REDLIB_DEFAULT_WIDE=off
#REDLIB_DEFAULT_POST_SORT=hot #REDLIB_DEFAULT_POST_SORT=hot
#REDLIB_DEFAULT_COMMENT_SORT=confidence #REDLIB_DEFAULT_COMMENT_SORT=confidence
#REDLIB_DEFAULT_BLUR_SPOILER=off
#REDLIB_DEFAULT_SHOW_NSFW=off #REDLIB_DEFAULT_SHOW_NSFW=off
#REDLIB_DEFAULT_BLUR_NSFW=off #REDLIB_DEFAULT_BLUR_NSFW=off
#REDLIB_DEFAULT_USE_HLS=off #REDLIB_DEFAULT_USE_HLS=off
#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=off (sub1+sub2+sub3)

31
scripts/load_test.py Normal file
View File

@ -0,0 +1,31 @@
import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
base_url = "http://localhost:8080"
full_path = f"{base_url}/r/politics"
ctr = 0
def fetch_url(url):
global ctr
response = requests.get(url)
ctr += 1
print(f"Request count: {ctr}")
return response
while full_path:
response = requests.get(full_path)
ctr += 1
print(f"Request count: {ctr}")
soup = BeautifulSoup(response.text, 'html.parser')
comment_links = soup.find_all('a', class_='post_comments')
comment_urls = [base_url + link['href'] for link in comment_links]
with ThreadPoolExecutor(max_workers=10) as executor:
executor.map(fetch_url, comment_urls)
next_link = soup.find('a', accesskey='N')
if next_link:
full_path = base_url + next_link['href']
else:
break

View File

@ -1,17 +1,21 @@
use arc_swap::ArcSwap;
use cached::proc_macro::cached; use cached::proc_macro::cached;
use futures_lite::future::block_on; use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt}; use futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::header::HeaderValue;
use hyper::StatusCode;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri}; use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector; use hyper_rustls::HttpsConnector;
use libflate::gzip; use libflate::gzip;
use log::error; use log::{error, trace, warn};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use percent_encoding::{percent_encode, CONTROLS}; use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value; use serde_json::Value;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicBool, AtomicU16};
use std::{io, result::Result}; use std::{io, result::Result};
use tokio::sync::RwLock;
use crate::dbg_msg; use crate::dbg_msg;
use crate::oauth::{force_refresh_token, token_daemon, Oauth}; use crate::oauth::{force_refresh_token, token_daemon, Oauth};
@ -19,23 +23,23 @@ use crate::server::RequestExt;
use crate::utils::format_url; use crate::utils::format_url;
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new() let https = hyper_rustls::HttpsConnectorBuilder::new().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)
}); });
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| { pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new()); let client = block_on(Oauth::new());
tokio::spawn(token_daemon()); tokio::spawn(token_daemon());
RwLock::new(client) ArcSwap::new(client.into())
}); });
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
/// Gets the canonical path for a resource on Reddit. This is accomplished by /// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`. /// making a `HEAD` request to Reddit at the path given in `path`.
/// ///
@ -52,10 +56,9 @@ pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
pub async fn canonical_path(path: String) -> Result<Option<String>, String> { pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
let res = reddit_head(path.clone(), true).await?; let res = reddit_head(path.clone(), true).await?;
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)),
@ -86,6 +89,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()
@ -170,8 +179,8 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// 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();
let (token, vendor_id, device_id, mut user_agent, loid) = { let (token, vendor_id, device_id, user_agent, loid) = {
let client = block_on(OAUTH_CLIENT.read()); let client = OAUTH_CLIENT.load_full();
( (
client.token.clone(), client.token.clone(),
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(), client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
@ -181,13 +190,6 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
) )
}; };
// Replace "Android" with a tricky word.
// Issues: #78/#115, #116
// If you include the word "Android", you will get a number of different errors
// I guess they don't expect mobile traffic on the endpoints we use
// Scrawled on wall for next poor soul: Run the test suite.
user_agent = user_agent.replace("Android", "Andr\u{200B}oid");
// Build request to Reddit. When making a GET, request gzip compression. // Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.) // (Reddit doesn't do brotli yet.)
let builder = Request::builder() let builder = Request::builder()
@ -222,12 +224,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
if !redirect { if !redirect {
return Ok(response); return Ok(response);
}; };
let location_header = response.headers().get(header::LOCATION);
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
return Err("Reddit response was invalid".to_string());
}
return request( return request(
method, method,
response location_header
.headers()
.get(header::LOCATION)
.map(|val| { .map(|val| {
// We need to make adjustments to the URI // We need to make adjustments to the URI
// we get back from Reddit. Namely, we // we get back from Reddit. Namely, we
@ -240,7 +243,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// required. // required.
// //
// 2. Percent-encode the path. // 2. Percent-encode the path.
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string(); let new_path = percent_encode(val.as_bytes(), CONTROLS)
.to_string()
.trim_start_matches(REDDIT_URL_BASE)
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
.to_string();
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" }) format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
}) })
.unwrap_or_default() .unwrap_or_default()
@ -251,6 +258,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.await; .await;
}; };
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
if response.status() == StatusCode::FORBIDDEN && response.headers().get("retry-after").unwrap_or(&HeaderValue::from_static("0")).to_str().unwrap_or("0") == "0" {
force_refresh_token().await;
return Err("Rate limit - try refreshing soon".to_string());
}
match response.headers().get(header::CONTENT_ENCODING) { match response.headers().get(header::CONTENT_ENCODING) {
// Content not compressed. // Content not compressed.
None => Ok(response), None => Ok(response),
@ -299,7 +312,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
} }
} }
Err(e) => { Err(e) => {
dbg_msg!("{} {}: {}", method, path, e); dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
Err(e.to_string()) Err(e.to_string())
} }
@ -314,19 +327,56 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
#[cached(size = 100, time = 30, result = true)] #[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> { pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// Closure to quickly build errors // Closure to quickly build errors
let err = |msg: &str, e: String| -> Result<Value, String> { let err = |msg: &str, e: String, path: String| -> Result<Value, String> {
// eprintln!("{} - {}: {}", url, msg, e); // eprintln!("{} - {}: {}", url, msg, e);
Err(format!("{msg}: {e}")) Err(format!("{msg}: {e} | {path}"))
}; };
// First, handle rolling over the OAUTH_CLIENT if need be.
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
let is_rolling_over = OAUTH_IS_ROLLING_OVER.load(Ordering::SeqCst);
if current_rate_limit < 10 && !is_rolling_over {
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
tokio::spawn(force_refresh_token());
}
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
// Fetch the url... // Fetch the url...
match reddit_get(path.clone(), quarantine).await { match reddit_get(path.clone(), quarantine).await {
Ok(response) => { Ok(response) => {
let status = response.status(); let status = response.status();
let reset: Option<String> = if let (Some(remaining), Some(reset), Some(used)) = (
response.headers().get("x-ratelimit-remaining").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
) {
trace!(
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
if is_rolling_over { "yes" } else { "no" },
);
Some(reset)
} else {
None
};
// asynchronously aggregate the chunks of the body // asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await { match hyper::body::aggregate(response).await {
Ok(body) => { Ok(body) => {
let has_remaining = body.has_remaining();
if !has_remaining {
// Rate limited, so spawn a force_refresh_token()
tokio::spawn(force_refresh_token());
return match reset {
Some(val) => Err(format!(
"Reddit rate limit exceeded. Try refreshing in a few seconds.\
Rate limit will reset in: {val}"
)),
None => Err("Reddit rate limit exceeded".to_string()),
};
}
// Parse the response from Reddit as JSON // Parse the response from Reddit as JSON
match serde_json::from_reader(body.reader()) { match serde_json::from_reader(body.reader()) {
Ok(value) => { Ok(value) => {
@ -339,7 +389,15 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await; let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string()); return Err("OAuth token has expired. Please refresh the page!".to_string());
} }
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"])) // Handle quarantined
if json["reason"] == "quarantined" {
return Err("quarantined".into());
}
// Handle gated
if json["reason"] == "gated" {
return Err("gated".into());
}
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else { } else {
Ok(json) Ok(json)
} }
@ -349,21 +407,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
if status.is_server_error() { if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string()) Err("Reddit is having issues, check if there's an outage".to_string())
} else { } else {
err("Failed to parse page JSON data", e.to_string()) err("Failed to parse page JSON data", e.to_string(), path)
} }
} }
} }
} }
Err(e) => err("Failed receiving body from Reddit", e.to_string()), Err(e) => err("Failed receiving body from Reddit", e.to_string(), path),
} }
} }
Err(e) => err("Couldn't send request to Reddit", e), Err(e) => err("Couldn't send request to Reddit", e, path),
} }
} }
#[cfg(test)]
static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL";
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_localization_popular() { async fn test_localization_popular() {
let val = json("/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL".to_string(), false).await.unwrap(); let val = json(POPULAR_URL.to_string(), false).await.unwrap();
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap()); assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
} }

View File

@ -52,6 +52,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")] #[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
pub(crate) default_post_sort: Option<String>, pub(crate) default_post_sort: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_BLUR_SPOILER")]
#[serde(alias = "LIBREDDIT_DEFAULT_BLUR_SPOILER")]
pub(crate) default_blur_spoiler: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")] #[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")] #[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
pub(crate) default_show_nsfw: Option<String>, pub(crate) default_show_nsfw: Option<String>,
@ -88,6 +92,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")] #[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
pub(crate) default_subscriptions: Option<String>, pub(crate) default_subscriptions: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_FILTERS")]
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>, pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -103,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 {
@ -135,6 +149,7 @@ impl Config {
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"), default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
default_wide: parse("REDLIB_DEFAULT_WIDE"), default_wide: parse("REDLIB_DEFAULT_WIDE"),
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"), default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
default_blur_spoiler: parse("REDLIB_DEFAULT_BLUR_SPOILER"),
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"), default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"), default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"), default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
@ -144,10 +159,13 @@ impl Config {
default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"), default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"), default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"), default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"), default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"), pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
} }
} }
} }
@ -161,6 +179,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(), "REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), "REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(), "REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
"REDLIB_DEFAULT_BLUR_SPOILER" => config.default_blur_spoiler.clone(),
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), "REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), "REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(), "REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
@ -171,10 +190,13 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(), "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(),
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(), "REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(), "REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(), "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(), "REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"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,
} }
} }
@ -243,6 +265,12 @@ fn test_default_subscriptions() {
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into())); assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
} }
#[test]
#[sealed_test(env = [("REDLIB_DEFAULT_FILTERS", "news+bestof")])]
fn test_default_filters() {
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
}
#[test] #[test]
#[sealed_test] #[sealed_test]
fn test_pushshift() { fn test_pushshift() {

View File

@ -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"]),
@ -142,11 +144,14 @@ impl InstanceInfo {
["Wide", &convert(&self.config.default_wide)], ["Wide", &convert(&self.config.default_wide)],
["Comment sort", &convert(&self.config.default_comment_sort)], ["Comment sort", &convert(&self.config.default_comment_sort)],
["Post sort", &convert(&self.config.default_post_sort)], ["Post sort", &convert(&self.config.default_post_sort)],
["Blur Spoiler", &convert(&self.config.default_blur_spoiler)],
["Show NSFW", &convert(&self.config.default_show_nsfw)], ["Show NSFW", &convert(&self.config.default_show_nsfw)],
["Blur NSFW", &convert(&self.config.default_blur_nsfw)], ["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
["Use HLS", &convert(&self.config.default_use_hls)], ["Use HLS", &convert(&self.config.default_use_hls)],
["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)],
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)], ["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)],
]) ])
.with_header_row(["Default preferences"]), .with_header_row(["Default preferences"]),
); );
@ -164,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
@ -175,11 +182,14 @@ impl InstanceInfo {
Default wide: {:?}\n Default wide: {:?}\n
Default comment sort: {:?}\n Default comment sort: {:?}\n
Default post sort: {:?}\n Default post sort: {:?}\n
Default blur Spoiler: {:?}\n
Default show NSFW: {:?}\n Default show NSFW: {:?}\n
Default blur NSFW: {:?}\n Default blur NSFW: {:?}\n
Default use HLS: {:?}\n Default use HLS: {:?}\n
Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n", Default subscriptions: {:?}\n
Default filters: {:?}\n",
self.package_name, self.package_name,
self.crate_version, self.crate_version,
self.git_commit, self.git_commit,
@ -187,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,
@ -198,11 +210,14 @@ impl InstanceInfo {
self.config.default_wide, self.config.default_wide,
self.config.default_comment_sort, self.config.default_comment_sort,
self.config.default_post_sort, self.config.default_post_sort,
self.config.default_blur_spoiler,
self.config.default_show_nsfw, self.config.default_show_nsfw,
self.config.default_blur_nsfw, self.config.default_blur_nsfw,
self.config.default_use_hls, self.config.default_use_hls,
self.config.default_ffmpeg_video_downloads,
self.config.default_hide_hls_notification, self.config.default_hide_hls_notification,
self.config.default_subscriptions, self.config.default_subscriptions,
self.config.default_filters,
) )
} }
StringType::Html => self.to_table(), StringType::Html => self.to_table(),

View File

@ -160,7 +160,7 @@ async fn main() {
.long("address") .long("address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Sets address to listen on") .help("Sets address to listen on")
.default_value("0.0.0.0") .default_value("[::]")
.num_args(1), .num_args(1),
) )
.arg( .arg(
@ -299,6 +299,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 +314,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")

View File

@ -1,12 +1,12 @@
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
use crate::{ use crate::{
client::{CLIENT, OAUTH_CLIENT}, client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
oauth_resources::ANDROID_APP_VERSION_LIST, oauth_resources::ANDROID_APP_VERSION_LIST,
}; };
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use hyper::{client, Body, Method, Request}; use hyper::{client, Body, Method, Request};
use log::info; use log::{info, trace};
use serde_json::json; use serde_json::json;
@ -98,21 +98,13 @@ impl Oauth {
Some(()) Some(())
} }
async fn refresh(&mut self) -> Option<()> {
// Refresh is actually just a subsequent login with the same headers (without the old token
// or anything). This logic is handled in login, so we just call login again.
let refresh = self.login().await;
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
refresh
}
} }
pub async fn token_daemon() { pub async fn token_daemon() {
// Monitor for refreshing token // Monitor for refreshing token
loop { loop {
// Get expiry time - be sure to not hold the read lock // Get expiry time - be sure to not hold the read lock
let expires_in = { OAUTH_CLIENT.read().await.expires_in }; let expires_in = { OAUTH_CLIENT.load_full().expires_in };
// sleep for the expiry time minus 2 minutes // sleep for the expiry time minus 2 minutes
let duration = Duration::from_secs(expires_in - 120); let duration = Duration::from_secs(expires_in - 120);
@ -125,13 +117,22 @@ pub async fn token_daemon() {
// Refresh token - in its own scope // Refresh token - in its own scope
{ {
OAUTH_CLIENT.write().await.refresh().await; force_refresh_token().await;
} }
} }
} }
pub async fn force_refresh_token() { pub async fn force_refresh_token() {
OAUTH_CLIENT.write().await.refresh().await; if OAUTH_IS_ROLLING_OVER.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
trace!("Skipping refresh token roll over, already in progress");
return;
}
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
let new_client = Oauth::new().await;
OAUTH_CLIENT.swap(new_client.into());
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
OAUTH_IS_ROLLING_OVER.store(false, Ordering::SeqCst);
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -179,21 +180,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client() { async fn test_oauth_client() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty()); assert!(!OAUTH_CLIENT.load_full().token.is_empty());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client_refresh() { async fn test_oauth_client_refresh() {
OAUTH_CLIENT.write().await.refresh().await.unwrap(); force_refresh_token().await;
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_token_exists() { async fn test_oauth_token_exists() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty()); assert!(!OAUTH_CLIENT.load_full().token.is_empty());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_oauth_headers_len() { async fn test_oauth_headers_len() {
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3); assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
} }
#[test] #[test]

View File

@ -19,7 +19,7 @@ struct SettingsTemplate {
// CONSTANTS // CONSTANTS
const PREFS: [&str; 18] = [ const PREFS: [&str; 19] = [
"theme", "theme",
"mascot", "mascot",
"front_page", "front_page",
@ -27,6 +27,7 @@ const PREFS: [&str; 18] = [
"wide", "wide",
"comment_sort", "comment_sort",
"post_sort", "post_sort",
"blur_spoiler",
"show_nsfw", "show_nsfw",
"blur_nsfw", "blur_nsfw",
"use_hls", "use_hls",
@ -122,7 +123,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
Some(value) => response.insert_cookie( Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone())) Cookie::build((name.to_owned(), value.clone()))
.path("/") .path("/")
.http_only(true) .http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52)) .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(), .into(),
), ),

View File

@ -1,3 +1,4 @@
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,
@ -119,7 +120,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
params.push_str(&format!("&geo_filter={geo_filter}")); params.push_str(&format!("&geo_filter={geo_filter}"));
} }
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default()); let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B"); let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
let filters = get_filters(&req); let filters = get_filters(&req);
@ -145,6 +146,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty(); let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
if sort == "new" {
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
}
Ok(template(&SubredditTemplate { Ok(template(&SubredditTemplate {
sub, sub,
posts, posts,
@ -455,8 +460,66 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
}) })
} }
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")] #[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,6 +2,7 @@
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 crate::{config, utils};
use askama::Template; use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use time::{macros::format_description, OffsetDateTime}; use time::{macros::format_description, OffsetDateTime};
@ -129,6 +130,56 @@ async fn user(name: &str) -> Result<User, String> {
}) })
} }
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use crate::utils::rewrite_urls;
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get user
let user_str = req.param("name").unwrap_or_default();
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
// Get path
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);
// Get user
let user_obj = user(&user_str).await.unwrap_or_default();
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(user_str)
.description(user_obj.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")] #[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,4 +1,5 @@
use crate::config::get_setting; #![allow(dead_code)]
use crate::config::{self, get_setting};
// //
// CRATES // CRATES
// //
@ -14,6 +15,7 @@ use serde_json::Value;
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;
@ -156,6 +158,7 @@ impl PollOption {
// Post flags with nsfw and stickied // Post flags with nsfw and stickied
pub struct Flags { pub struct Flags {
pub spoiler: bool,
pub nsfw: bool, pub nsfw: bool,
pub stickied: bool, pub stickied: bool,
} }
@ -167,6 +170,7 @@ pub struct Media {
pub width: i64, pub width: i64,
pub height: i64, pub height: i64,
pub poster: String, pub poster: String,
pub download_name: String,
} }
impl Media { impl Media {
@ -233,6 +237,15 @@ impl Media {
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default())); let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
format!("redlib_{permalink_base}_{media_url_base}")
} else {
String::new()
};
( (
post_type.to_string(), post_type.to_string(),
Self { Self {
@ -243,6 +256,7 @@ impl Media {
width: source["width"].as_i64().unwrap_or_default(), width: source["width"].as_i64().unwrap_or_default(),
height: source["height"].as_i64().unwrap_or_default(), height: source["height"].as_i64().unwrap_or_default(),
poster: format_url(source["url"].as_str().unwrap_or_default()), poster: format_url(source["url"].as_str().unwrap_or_default()),
download_name,
}, },
gallery, gallery,
) )
@ -296,6 +310,7 @@ pub struct Post {
pub body: String, pub body: String,
pub author: Author, pub author: Author,
pub permalink: String, pub permalink: String,
pub link_title: String,
pub poll: Option<Poll>, pub poll: Option<Poll>,
pub score: (String, String), pub score: (String, String),
pub upvote_ratio: i64, pub upvote_ratio: i64,
@ -307,11 +322,13 @@ pub struct Post {
pub domain: String, pub domain: String,
pub rel_time: String, pub rel_time: String,
pub created: String, pub created: String,
pub created_ts: u64,
pub num_duplicates: u64, pub num_duplicates: u64,
pub comments: (String, String), pub comments: (String, String),
pub gallery: Vec<GalleryMedia>, pub gallery: Vec<GalleryMedia>,
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,
} }
@ -338,6 +355,7 @@ impl Post {
let data = &post["data"]; let data = &post["data"];
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default()); let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
let score = data["score"].as_i64().unwrap_or_default(); let score = data["score"].as_i64().unwrap_or_default();
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0; let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = val(post, "title"); let title = val(post, "title");
@ -384,6 +402,7 @@ impl Post {
width: data["thumbnail_width"].as_i64().unwrap_or_default(), width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(), height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(), poster: String::new(),
download_name: String::new(),
}, },
media, media,
domain: val(post, "domain"), domain: val(post, "domain"),
@ -402,22 +421,25 @@ impl Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: data["spoiler"].as_bool().unwrap_or_default(),
nsfw: data["over_18"].as_bool().unwrap_or_default(), nsfw: data["over_18"].as_bool().unwrap_or_default(),
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(), stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
}, },
permalink: val(post, "permalink"), permalink: val(post, "permalink"),
link_title: val(post, "link_title"),
poll: Poll::parse(&data["poll_data"]), poll: Poll::parse(&data["poll_data"]),
rel_time, rel_time,
created, created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0), num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()), comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
gallery, gallery,
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()))
} }
} }
@ -576,6 +598,7 @@ pub struct Preferences {
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
pub blur_spoiler: String,
pub show_nsfw: String, pub show_nsfw: String,
pub blur_nsfw: String, pub blur_nsfw: String,
pub hide_hls_notification: String, pub hide_hls_notification: String,
@ -628,6 +651,7 @@ impl Preferences {
front_page: setting(req, "front_page"), front_page: setting(req, "front_page"),
layout: setting(req, "layout"), layout: setting(req, "layout"),
wide: setting(req, "wide"), wide: setting(req, "wide"),
blur_spoiler: setting(req, "blur_spoiler"),
show_nsfw: setting(req, "show_nsfw"), show_nsfw: setting(req, "show_nsfw"),
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"), hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
blur_nsfw: setting(req, "blur_nsfw"), blur_nsfw: setting(req, "blur_nsfw"),
@ -686,6 +710,8 @@ pub async fn parse_post(post: &Value) -> Post {
// Determine the type of media along with the media URL // Determine the type of media along with the media URL
let (post_type, media, gallery) = Media::parse(&post["data"]).await; let (post_type, media, gallery) = Media::parse(&post["data"]).await;
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]); let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
let permalink = val(post, "permalink"); let permalink = val(post, "permalink");
@ -722,6 +748,7 @@ pub async fn parse_post(post: &Value) -> Post {
distinguished: val(post, "distinguished"), distinguished: val(post, "distinguished"),
}, },
permalink, permalink,
link_title: val(post, "link_title"),
poll, poll,
score: format_num(score), score: format_num(score),
upvote_ratio: ratio as i64, upvote_ratio: ratio as i64,
@ -733,6 +760,7 @@ pub async fn parse_post(post: &Value) -> Post {
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(), width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(), height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(), poster: String::new(),
download_name: String::new(),
}, },
flair: Flair { flair: Flair {
flair_parts: FlairPart::parse( flair_parts: FlairPart::parse(
@ -749,18 +777,21 @@ pub async fn parse_post(post: &Value) -> Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: post["data"]["spoiler"].as_bool().unwrap_or_default(),
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false), stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
}, },
domain: val(post, "domain"), domain: val(post, "domain"),
rel_time, rel_time,
created, created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0), num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()), comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
gallery, gallery,
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()),
} }
} }
@ -1046,7 +1077,7 @@ pub fn redirect(path: &str) -> Response<Body> {
/// Renders a generic error landing page. /// Renders a generic error landing page.
pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> { pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
error!("Error page rendered: {msg}"); error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
let url = req.uri().to_string(); let url = req.uri().to_string();
let body = ErrorTemplate { let body = ErrorTemplate {
msg: msg.to_string(), msg: msg.to_string(),
@ -1073,6 +1104,28 @@ pub fn sfw_only() -> bool {
} }
} }
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
/// value `on`.
///
/// If this variable is set as such, the instance will block all robots in robots.txt and
/// insert the noindex, nofollow meta tag on every page.
pub fn disable_indexing() -> bool {
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
Some(val) => val == "on",
None => false,
}
}
// Determines if a request shoud redirect to a nsfw landing gate. // 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();
@ -1114,6 +1167,34 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default()) Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
} }
// Returns the last (non-empty) segment of a path string
pub fn url_path_basename(path: &str) -> String {
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
if url_result.is_err() {
path.to_string()
} else {
let mut url = url_result.unwrap();
url.path_segments_mut().unwrap().pop_if_empty();
url.path_segments().unwrap().last().unwrap().to_string()
}
}
// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{format_num, format_url, rewrite_urls}; use super::{format_num, format_url, rewrite_urls};
@ -1222,3 +1303,19 @@ fn test_rewriting_image_links() {
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#; let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
assert_eq!(rewrite_urls(input), output); assert_eq!(rewrite_urls(input), output);
} }
#[test]
fn test_url_path_basename() {
// without trailing slash
assert_eq!(url_path_basename("/first/last"), "last");
// with trailing slash
assert_eq!(url_path_basename("/first/last/"), "last");
// with query parameters
assert_eq!(url_path_basename("/first/last/?some=query"), "last");
// file path
assert_eq!(url_path_basename("/cdn/image.jpg"), "image.jpg");
// when a full url is passed instead of just a path
assert_eq!(url_path_basename("https://doma.in/first/last"), "last");
// empty path
assert_eq!(url_path_basename("/"), "");
}

File diff suppressed because it is too large Load Diff

View File

@ -115,7 +115,7 @@ let ffmpeg = null;
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title) var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
const data = await ffmpeg.readFile('output.mp4'); const data = await ffmpeg.readFile('output.mp4');
saveAs(new Blob([data.buffer]),filename); saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
return return
} }
function saveAs(blob, filename) { // Yeah ok... function saveAs(blob, filename) { // Yeah ok...

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 -->
@ -28,7 +31,7 @@
</head> </head>
<body class=" <body class="
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %} {% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
{% if prefs.wide == "on" %} wide{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %} wide{% endif %}
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %} {% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}"> {% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<!-- NAVIGATION BAR --> <!-- NAVIGATION BAR -->

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

@ -6,6 +6,10 @@
<h1>{{ msg }}</h1> <h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3> <h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br /> <br />
<h3>Expected something to work? <a
href="https://github.com/redlib-org/redlib/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=%F0%9F%90%9B+Bug+Report%3A+{{ msg }}">Report
an issue</a></h3>
<br />
<h3>Head back <a href="/">home</a>?</h3> <h3>Head back <a href="/">home</a>?</h3>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,11 +6,11 @@
<h1> <h1>
&#128561; &#128561;
{% if res_type == crate::utils::ResourceType::Subreddit %} {% if res_type == crate::utils::ResourceType::Subreddit %}
r/{{ res }} is a NSFW community! r/{{ res }} is a <b class="nsfw-tag">NSFW</b> community!
{% else if res_type == crate::utils::ResourceType::User %} {% else if res_type == crate::utils::ResourceType::User %}
u/{{ res }}'s content is NSFW! u/{{ res }}'s content is <b class="nsfw-tag">NSFW</b>!
{% else if res_type == crate::utils::ResourceType::Post %} {% else if res_type == crate::utils::ResourceType::Post %}
This post is NSFW! This post is <b class="nsfw-tag">NSFW</b>!
{% endif %} {% endif %}
</h1> </h1>
<br /> <br />
@ -20,6 +20,7 @@
This instance of Redlib is SFW-only.</p> This instance of Redlib is SFW-only.</p>
{% else %} {% else %}
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br> Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br>
<div>Alternatively <a href="/settings/update/?show_nsfw=on&redirect={{self.url[1..self.url.len()]}}">enable NSFW posts</a> now and view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}profile{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %} immediately</div>
{% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %} {% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %}
{% endif %} {% endif %}
</p> </p>

View File

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

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 %}
@ -37,13 +41,14 @@
<div class="prefs-group"> <div class="prefs-group">
<label for="layout">Layout:</label> <label for="layout">Layout:</label>
<select name="layout" id="layout"> <select name="layout" id="layout">
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %} {% call utils::options(prefs.layout, ["card", "clean", "compact", "old", "waterfall"], "card") %}
</select> </select>
</div> </div>
<div class="prefs-group"> <div class="prefs-group">
<label for="wide">Wide UI:</label> <label for="wide">Wide UI:</label>
<input type="hidden" value="off" name="wide"> <input type="hidden" value="off" name="wide">
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}> <input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -60,6 +65,11 @@
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select> </select>
</div> </div>
<div class="prefs-group">
<label for="blur_spoiler">Blur spoiler previews:</label>
<input type="hidden" value="off" name="blur_spoiler">
<input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}>
</div>
{% if !crate::utils::sfw_only() %} {% if !crate::utils::sfw_only() %}
<div class="prefs-group"> <div class="prefs-group">
<label for="show_nsfw">Show NSFW posts:</label> <label for="show_nsfw">Show NSFW posts:</label>
@ -132,6 +142,10 @@
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>
</form> </form>
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div>
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs"> <div class="prefs" id="settings_subs">
<legend>Subscribed Feeds</legend> <legend>Subscribed Feeds</legend>
@ -162,11 +176,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -134,7 +134,13 @@
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
</div> </div>
{% endif %}
</div> </div>
</details> </details>
<details class="panel" id="sidebar" open> <details class="panel" id="sidebar" open>

View File

@ -1,30 +1,33 @@
{% 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 search %} block subscriptions %} {% call utils::sub_list("") %} {% endblock %} {% block
{% call utils::search("".to_owned(), "") %} body %}
{% endblock %}
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %}
{% block subscriptions %}
{% call utils::sub_list("") %}
{% endblock %}
{% block body %}
<main> <main>
{% if !is_filtered %} {% if !is_filtered %}
<div id="column_one"> <div id="column_one">
<form id="sort"> <form id="sort">
<div id="listing_options"> <div id="listing_options">
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %} {% call utils::sort(["/user/", user.name.as_str()].concat(),
["overview", "comments", "submitted"], listing) %}
</div> </div>
<select id="sort_select" name="sort"> <select id="sort_select" name="sort">
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %} {% call utils::options(sort.0, ["hot", "new", "top",
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t"> "controversial"], "") %}</select
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %} >{% if sort.0 == "top" || sort.0 == "controversial" %}<select
</select>{% endif %}<button id="sort_submit" class="submit"> id="timeframe"
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round"> 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="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" /> <path d="M75 15 L100 50 L75 85" />
&rarr; &rarr;
@ -33,44 +36,52 @@
</form> </form>
{% if all_posts_hidden_nsfw %} {% if all_posts_hidden_nsfw %}
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center> <center>
{% endif %} All posts are hidden because they are NSFW. Enable "Show NSFW posts"
in settings to view.
{% if no_posts %} </center>
{% endif %} {% if no_posts %}
<center>No posts were found.</center> <center>No posts were found.</center>
{% endif %} {% endif %} {% if all_posts_filtered %}
{% if all_posts_filtered %}
<center>(All content on this page has been filtered)</center> <center>(All content on this page has been filtered)</center>
{% else %} {% else %}
<div id="posts"> <div id="posts">
{% for post in posts %} {% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
"on" %} {% else if !post.title.is_empty() %} {% call
{% if post.flags.nsfw && prefs.show_nsfw != "on" %} utils::post_in_list(post) %} {% else %}
{% else if !post.title.is_empty() %} <div class="comment user-comment">
{% call utils::post_in_list(post) %}
{% else %}
<div class="comment">
<div class="comment_left"> <div class="comment_left">
<p class="comment_score" title="{{ post.score.1 }}"> <p class="comment_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %} {% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
{{ post.score.0 }} else %} &#x2022; {% endif %}
{% 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 }}">Comment on r/{{ post.community }}</a> <a
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span> class="comment_link"
href="{{ post.permalink }}"
title="{{ post.link_title }}"
>{{ post.link_title }}</a
>
<div class="user_comment_data_divider">
<span class="created-in">&nbsp;in&nbsp;</span>
<a
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> </summary>
<p class="comment_body">{{ post.body|safe }}</p> <p class="comment_body">{{ post.body|safe }}</p>
</details> </details>
</div> </div>
{% endif %} {% endif %} {% endfor %}
{% 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>
@ -84,11 +95,17 @@
<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 %} {% if ends.1 != "" %}
<a
href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
accesskey="N"
>NEXT</a
>
{% endif %} {% endif %}
</footer> </footer>
</div> </div>
@ -98,7 +115,12 @@
<center>(Content from u/{{ user.name }} has been filtered)</center> <center>(Content from u/{{ user.name }} has been filtered)</center>
{% endif %} {% endif %}
<div class="panel" id="user"> <div class="panel" id="user">
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon"> <img
loading="lazy"
id="user_icon"
src="{{ user.icon }}"
alt="User icon"
/>
<h1 id="user_title">{{ user.title }}</h1> <h1 id="user_title">{{ user.title }}</h1>
<p id="user_name">u/{{ user.name }}</p> <p id="user_name">u/{{ user.name }}</p>
<div id="user_description">{{ user.description }}</div> <div id="user_description">{{ user.description }}</div>
@ -112,26 +134,48 @@
{% let name = ["u_", user.name.as_str()].join("") %} {% let name = ["u_", user.name.as_str()].join("") %}
<div id="user_subscription"> <div id="user_subscription">
{% if prefs.subscriptions.contains(name) %} {% if prefs.subscriptions.contains(name) %}
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST"> <form
action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
method="POST"
>
<button class="unsubscribe">Unfollow</button> <button class="unsubscribe">Unfollow</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST"> <form
action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
method="POST"
>
<button class="subscribe">Follow</button> <button class="subscribe">Follow</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
<div id="user_filter"> <div id="user_filter">
{% if prefs.filters.contains(name) %} {% if prefs.filters.contains(name) %}
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST"> <form
action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}"
method="POST"
>
<button class="unfilter">Unfilter</button> <button class="unfilter">Unfilter</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST"> <form
action="/r/{{ name }}/filter?redirect={{ redirect_url }}"
method="POST"
>
<button class="filter">Filter</button> <button class="filter">Filter</button>
</form> </form>
{% endif %} {% endif %}
</div> </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>
</div> </div>
</aside> </aside>

View File

@ -62,8 +62,9 @@
{%- endmacro %} {%- endmacro %}
{% macro post(post) -%} {% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT --> <!-- POST CONTENT -->
<div class="post highlighted"> <div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
<p class="post_header"> <p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
@ -86,13 +87,14 @@
{% 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 %}
</h1> </h1>
<!-- POST MEDIA --> <!-- POST MEDIA -->
@ -101,7 +103,7 @@
<div class="post_media_content"> <div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image" > <a href="{{ post.media.url }}" class="post_media_image" >
{% if post.media.height == 0 || post.media.width == 0 %} {% if post.media.height == 0 || post.media.width == 0 %}
<!-- i.redd.it images speical case --> <!-- i.redd.it images special case -->
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
@ -165,13 +167,32 @@
<span class="label"> Upvotes</span></div> <span class="label"> Upvotes</span></div>
<div class="post_footer"> <div class="post_footer">
<ul id="post_links"> <ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li> <li>
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li> <a href="{{ post.permalink }}">
<span class="desktop_item">perma</span>link
</a>
</li>
{% if post.num_duplicates > 0 %} {% if post.num_duplicates > 0 %}
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li> <li>
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li> <a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
dup<span class="desktop_item">licat</span>es
</a>
</li>
{% endif %}
{% 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 %} {% endif %}
{% call external_reddit_link(post.permalink) %} {% call external_reddit_link(post.permalink) %}
{% if post.media.download_name != "" %}
<li>
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
<span class="mobile_item">dl</span>
<span class="desktop_item">download</span>
</a>
</li>
{% endif %}
</ul> </ul>
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p> <p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
</div> </div>
@ -179,8 +200,7 @@
{%- endmacro %} {%- endmacro %}
{% macro external_reddit_link(permalink) %} {% macro external_reddit_link(permalink) %}
{% for dev_type in ["desktop", "mobile"] %} <li>
<li class="{{ dev_type }}_item">
<a <a
{% if prefs.disable_visit_reddit_confirmation != "on" %} {% if prefs.disable_visit_reddit_confirmation != "on" %}
href="#popup" href="#popup"
@ -194,11 +214,11 @@
{% call visit_reddit_confirmation(permalink) %} {% call visit_reddit_confirmation(permalink) %}
{% endif %} {% endif %}
</li> </li>
{% endfor %}
{% endmacro %} {% endmacro %}
{% macro post_in_list(post) -%} {% macro post_in_list(post) -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}"> {% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
<p class="post_header"> <p class="post_header">
{% let community -%} {% let community -%}
{% if post.community.starts_with("u_") -%} {% if post.community.starts_with("u_") -%}
@ -226,10 +246,10 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a> dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} <a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h2> </h2>
<!-- POST MEDIA/THUMBNAIL --> <!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %} {% if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && post.post_type == "image" %}
<div class="post_media_content"> <div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" > <a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
{% if post.media.height == 0 || post.media.width == 0 %} {% if post.media.height == 0 || post.media.width == 0 %}
@ -237,7 +257,6 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -249,21 +268,17 @@
{% endif %} {% endif %}
</a> </a>
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || post.post_type == "video") %}
<div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || 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.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none"> <video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %} {% endif %}
@ -276,7 +291,7 @@
</svg> </svg>
{% else %} {% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;"> <div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg"> <svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/> <image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc> <desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/> <img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>