Compare commits

...

52 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
24 changed files with 2270 additions and 1331 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target

View File

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

View File

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

308
Cargo.lock generated
View File

@ -71,6 +71,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "askama"
version = "0.12.1"
@ -92,7 +98,7 @@ dependencies = [
"mime_guess",
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -118,7 +124,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
name = "atom_syndication"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
dependencies = [
"chrono",
"derive_builder 0.20.0",
"diligent-date-parser",
"never",
"quick-xml 0.31.0",
]
[[package]]
@ -142,6 +161,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@ -230,10 +255,10 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669"
dependencies = [
"darling",
"darling 0.20.9",
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -254,6 +279,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]]
name = "clap"
version = "4.5.7"
@ -342,14 +376,38 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core 0.14.4",
"darling_macro 0.14.4",
]
[[package]]
name = "darling"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.20.9",
"darling_macro 0.20.9",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
]
[[package]]
@ -362,8 +420,19 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
"strsim 0.11.1",
"syn 2.0.68",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core 0.14.4",
"quote",
"syn 1.0.109",
]
[[package]]
@ -372,9 +441,9 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
dependencies = [
"darling_core",
"darling_core 0.20.9",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -392,6 +461,68 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_builder"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro 0.12.0",
]
[[package]]
name = "derive_builder"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
dependencies = [
"derive_builder_macro 0.20.0",
]
[[package]]
name = "derive_builder_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
dependencies = [
"darling 0.20.9",
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core 0.12.0",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core 0.20.0",
"syn 2.0.68",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -402,12 +533,30 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "diligent-date-parser"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
dependencies = [
"chrono",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.10.2"
@ -679,9 +828,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.25.0"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
@ -689,7 +838,6 @@ dependencies = [
"log",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
]
@ -856,6 +1004,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "nom"
version = "7.1.3"
@ -872,6 +1026,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
@ -996,6 +1159,26 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quote"
version = "1.0.36"
@ -1041,10 +1224,11 @@ dependencies = [
[[package]]
name = "redsunlib"
version = "0.35.1"
version = "0.35.2"
dependencies = [
"arc-swap",
"askama",
"base64",
"base64 0.22.1",
"brotli",
"build_html",
"cached",
@ -1063,6 +1247,7 @@ dependencies = [
"pretty_env_logger",
"regex",
"route-recognizer",
"rss",
"rust-embed",
"sealed_test",
"serde",
@ -1131,6 +1316,18 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
[[package]]
name = "rss"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
dependencies = [
"atom_syndication",
"derive_builder 0.12.0",
"never",
"quick-xml 0.30.0",
]
[[package]]
name = "rust-embed"
version = "8.4.0"
@ -1151,7 +1348,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"syn 2.0.68",
"walkdir",
]
@ -1187,55 +1384,44 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.22.4"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
"sct",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.0"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64",
"rustls-pki-types",
"base64 0.21.7",
]
[[package]]
name = "rustls-pki-types"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]]
name = "rustls-webpki"
version = "0.102.4"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
@ -1281,6 +1467,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sealed_test"
version = "1.1.0"
@ -1300,7 +1496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
dependencies = [
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1343,7 +1539,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1430,6 +1626,12 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
@ -1437,10 +1639,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
@ -1491,7 +1698,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1569,17 +1776,16 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
name = "tokio-rustls"
version = "0.25.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls",
"rustls-pki-types",
"tokio",
]
@ -1943,11 +2149,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

View File

@ -1,9 +1,9 @@
[package]
name = "redsunlib"
description = " Alternative private front-end to Reddit"
license = "AGPL-3.0"
repository = "https://git.stardust.wtf/iridium/redlib"
version = "0.35.1"
license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.2"
authors = [
"Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
@ -22,7 +22,7 @@ serde = { version = "1.0.193", features = ["derive"] }
cookie = "0.18.0"
futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] }
hyper-rustls = "0.25.0"
hyper-rustls = "0.24.2"
percent-encoding = "2.3.1"
route-recognizer = "0.3.1"
serde_json = "1.0.108"
@ -42,6 +42,9 @@ fastrand = "2.0.1"
log = "0.4.20"
pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1"
[dev-dependencies]
lipsum = "0.9.0"

View File

@ -312,7 +312,8 @@ Assign a default value for each instance-specific setting by passing environment
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
## Default user settings
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
@ -322,7 +323,7 @@ Assign a default value for each user-modifiable setting by passing environment v
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
@ -337,4 +338,5 @@ Assign a default value for each user-modifiable setting by passing environment v
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
| `HIDE_SCORE` | `["on", "off"]` | `off` |
| `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |

View File

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

View File

@ -1,7 +1,10 @@
use arc_swap::ArcSwap;
use cached::proc_macro::cached;
use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector;
use hyper::header::HeaderValue;
use hyper::StatusCode;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector;
use libflate::gzip;
@ -11,9 +14,8 @@ use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicU16, Ordering::SeqCst};
use std::sync::atomic::{AtomicBool, AtomicU16};
use std::{io, result::Result};
use tokio::sync::RwLock;
use crate::dbg_msg;
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
@ -21,25 +23,23 @@ use crate::server::RequestExt;
use crate::utils::format_url;
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.expect("No native root certificates found")
.https_only()
.enable_http1()
.build();
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
client::Client::builder().build(https)
});
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new());
tokio::spawn(token_daemon());
RwLock::new(client)
ArcSwap::new(client.into())
});
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
/// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`.
///
@ -56,10 +56,9 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
let res = reddit_head(path.clone(), true).await?;
let status = res.status().as_u16();
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
match status {
429 => Err("Too many requests.".to_string()),
// If Reddit responds with a 2xx, then the path is already canonical.
200..=299 => Ok(Some(path)),
@ -90,6 +89,12 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// as above), return a None.
300..=399 => Ok(None),
// Rate limiting
429 => Err("Too many requests.".to_string()),
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
403 if policy_error => Err("Too many requests.".to_string()),
_ => Ok(
res
.headers()
@ -175,7 +180,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
let client: Client<_, Body> = CLIENT.clone();
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.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
@ -219,12 +224,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
if !redirect {
return Ok(response);
};
let location_header = response.headers().get(header::LOCATION);
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
return Err("Reddit response was invalid".to_string());
}
return request(
method,
response
.headers()
.get(header::LOCATION)
location_header
.map(|val| {
// We need to make adjustments to the URI
// we get back from Reddit. Namely, we
@ -237,7 +243,11 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// required.
//
// 2. Percent-encode the path.
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string();
let new_path = percent_encode(val.as_bytes(), CONTROLS)
.to_string()
.trim_start_matches(REDDIT_URL_BASE)
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
.to_string();
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
})
.unwrap_or_default()
@ -248,6 +258,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.await;
};
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
if response.status() == StatusCode::FORBIDDEN && response.headers().get("retry-after").unwrap_or(&HeaderValue::from_static("0")).to_str().unwrap_or("0") == "0" {
force_refresh_token().await;
return Err("Rate limit - try refreshing soon".to_string());
}
match response.headers().get(header::CONTENT_ENCODING) {
// Content not compressed.
None => Ok(response),
@ -296,7 +312,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
}
}
Err(e) => {
dbg_msg!("{} {}: {}", method, path, e);
dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
Err(e.to_string())
}
@ -318,36 +334,28 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// First, handle rolling over the OAUTH_CLIENT if need be.
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
if current_rate_limit < 10 {
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()");
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
tokio::spawn(force_refresh_token());
}
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
// Fetch the url...
match reddit_get(path.clone(), quarantine).await {
Ok(response) => {
let status = response.status();
// Ratelimit remaining
if let Some(Ok(remaining)) = response.headers().get("x-ratelimit-remaining").map(|val| val.to_str()) {
trace!("Ratelimit remaining: {}", remaining);
if let Ok(remaining) = remaining.parse::<f32>().map(|f| f.round() as u16) {
OAUTH_RATELIMIT_REMAINING.store(remaining, SeqCst);
} else {
warn!("Failed to parse rate limit {remaining} from header.");
}
}
// Ratelimit used
if let Some(Ok(used)) = response.headers().get("x-ratelimit-used").map(|val| val.to_str()) {
trace!("Ratelimit used: {}", used);
}
// Ratelimit reset
let reset = if let Some(Ok(reset)) = response.headers().get("x-ratelimit-reset").map(|val| val.to_str()) {
trace!("Ratelimit reset: {}", reset);
Some(reset.to_string())
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
};
@ -358,8 +366,13 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
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. Will reset in: {val}")),
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()),
};
}
@ -376,6 +389,14 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string());
}
// Handle quarantined
if json["reason"] == "quarantined" {
return Err("quarantined".into());
}
// Handle gated
if json["reason"] == "gated" {
return Err("gated".into());
}
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else {
Ok(json)

View File

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

View File

@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)],
["RSS enabled", &convert(&self.config.enable_rss)],
["Full URL", &convert(&self.config.full_url)],
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
])
.with_header_row(["Settings"]),
@ -167,6 +169,8 @@ impl InstanceInfo {
Compile mode: {}\n
SFW only: {:?}\n
Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n
Banner: {:?}\n
Hide awards: {:?}\n
@ -193,6 +197,8 @@ impl InstanceInfo {
self.deploy_unix_ts,
self.compile_mode,
self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift,
self.config.banner,
self.config.default_hide_awards,

View File

@ -299,6 +299,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -313,6 +314,9 @@ async fn main() {
// Mascots
app.at("/mascot/:name").get(|r| mascot_image(r).boxed());
// RSS Subscriptions
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());
// Subreddit services
app
.at("/r/:sub")

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
use crate::{
client::{CLIENT, OAUTH_CLIENT, OAUTH_RATELIMIT_REMAINING},
client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
oauth_resources::ANDROID_APP_VERSION_LIST,
};
use base64::{engine::general_purpose, Engine as _};
@ -98,21 +98,13 @@ impl Oauth {
Some(())
}
async fn refresh(&mut self) -> Option<()> {
// Refresh is actually just a subsequent login with the same headers (without the old token
// or anything). This logic is handled in login, so we just call login again.
let refresh = self.login().await;
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
refresh
}
}
pub async fn token_daemon() {
// Monitor for refreshing token
loop {
// Get expiry time - be sure to not hold the read lock
let expires_in = { OAUTH_CLIENT.read().await.expires_in };
let expires_in = { OAUTH_CLIENT.load_full().expires_in };
// sleep for the expiry time minus 2 minutes
let duration = Duration::from_secs(expires_in - 120);
@ -125,14 +117,22 @@ pub async fn token_daemon() {
// Refresh token - in its own scope
{
OAUTH_CLIENT.write().await.refresh().await;
force_refresh_token().await;
}
}
}
pub async fn force_refresh_token() {
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));
OAUTH_CLIENT.write().await.refresh().await;
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)]
@ -180,21 +180,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client_refresh() {
OAUTH_CLIENT.write().await.refresh().await.unwrap();
force_refresh_token().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_token_exists() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_headers_len() {
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3);
assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
}
#[test]

View File

@ -1,3 +1,4 @@
use crate::{config, utils};
// CRATES
use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
@ -64,7 +65,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
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));
let mut sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
let sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
if subscribed.is_empty() {
"popular".to_string()
} else {
@ -84,11 +85,6 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
return Ok(redirect(&["/user/", &sub_name[2..]].concat()));
}
// If multi-sub, replace + with url encoded +
if sub_name.contains('+') {
sub_name = sub_name.replace('+', "%2B");
}
// Request subreddit metadata
let sub = if !sub_name.contains('+') && sub_name != subscribed && sub_name != "popular" && sub_name != "all" {
// Regular subreddit
@ -124,7 +120,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
params.push_str(&format!("&geo_filter={geo_filter}"));
}
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default());
let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
let filters = get_filters(&req);
@ -150,6 +146,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
if sort == "new" {
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
}
Ok(template(&SubredditTemplate {
sub,
posts,
@ -460,8 +460,66 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
})
}
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await;
assert!(subreddit.is_ok());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_gated_and_quarantined() {
let quarantined = subreddit("edgy", true).await;
assert!(quarantined.is_ok());
let gated = subreddit("drugs", true).await;
assert!(gated.is_ok());
}

View File

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

View File

@ -1,5 +1,5 @@
#![allow(dead_code)]
use crate::config::get_setting;
use crate::config::{self, get_setting};
//
// CRATES
//
@ -15,6 +15,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::env;
use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url;
@ -169,6 +170,7 @@ pub struct Media {
pub width: i64,
pub height: i64,
pub poster: String,
pub download_name: String,
}
impl Media {
@ -235,6 +237,15 @@ impl Media {
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
format!("redlib_{permalink_base}_{media_url_base}")
} else {
String::new()
};
(
post_type.to_string(),
Self {
@ -245,6 +256,7 @@ impl Media {
width: source["width"].as_i64().unwrap_or_default(),
height: source["height"].as_i64().unwrap_or_default(),
poster: format_url(source["url"].as_str().unwrap_or_default()),
download_name,
},
gallery,
)
@ -298,6 +310,7 @@ pub struct Post {
pub body: String,
pub author: Author,
pub permalink: String,
pub link_title: String,
pub poll: Option<Poll>,
pub score: (String, String),
pub upvote_ratio: i64,
@ -309,11 +322,13 @@ pub struct Post {
pub domain: String,
pub rel_time: String,
pub created: String,
pub created_ts: u64,
pub num_duplicates: u64,
pub comments: (String, String),
pub gallery: Vec<GalleryMedia>,
pub awards: Awards,
pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String,
}
@ -340,6 +355,7 @@ impl Post {
let data = &post["data"];
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
let score = data["score"].as_i64().unwrap_or_default();
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = val(post, "title");
@ -386,6 +402,7 @@ impl Post {
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(),
download_name: String::new(),
},
media,
domain: val(post, "domain"),
@ -409,18 +426,20 @@ impl Post {
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
},
permalink: val(post, "permalink"),
link_title: val(post, "link_title"),
poll: Poll::parse(&data["poll_data"]),
rel_time,
created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
gallery,
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
});
}
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
}
}
@ -691,6 +710,8 @@ pub async fn parse_post(post: &Value) -> Post {
// Determine the type of media along with the media URL
let (post_type, media, gallery) = Media::parse(&post["data"]).await;
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
let permalink = val(post, "permalink");
@ -727,6 +748,7 @@ pub async fn parse_post(post: &Value) -> Post {
distinguished: val(post, "distinguished"),
},
permalink,
link_title: val(post, "link_title"),
poll,
score: format_num(score),
upvote_ratio: ratio as i64,
@ -738,6 +760,7 @@ pub async fn parse_post(post: &Value) -> Post {
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(),
download_name: String::new(),
},
flair: Flair {
flair_parts: FlairPart::parse(
@ -761,12 +784,14 @@ pub async fn parse_post(post: &Value) -> Post {
domain: val(post, "domain"),
rel_time,
created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
gallery,
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
}
}
@ -1079,6 +1104,28 @@ pub fn sfw_only() -> bool {
}
}
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
/// value `on`.
///
/// If this variable is set as such, the instance will block all robots in robots.txt and
/// insert the noindex, nofollow meta tag on every page.
pub fn disable_indexing() -> bool {
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
Some(val) => val == "on",
None => false,
}
}
// Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only();
@ -1120,6 +1167,34 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
}
// Returns the last (non-empty) segment of a path string
pub fn url_path_basename(path: &str) -> String {
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
if url_result.is_err() {
path.to_string()
} else {
let mut url = url_result.unwrap();
url.path_segments_mut().unwrap().pop_if_empty();
url.path_segments().unwrap().last().unwrap().to_string()
}
}
// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}
#[cfg(test)]
mod tests {
use super::{format_num, format_url, rewrite_urls};
@ -1228,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"#;
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)
const data = await ffmpeg.readFile('output.mp4');
saveAs(new Blob([data.buffer]),filename);
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
return
}
function saveAs(blob, filename) { // Yeah ok...

View File

@ -8,6 +8,9 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="description" content="View on Redlib, an alternative private front-end to Reddit.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if crate::utils::disable_indexing() %}
<meta name="robots" content="noindex, nofollow">
{% endif %}
<!-- General PWA -->
<meta name="theme-color" content="#1F1F1F">
<!-- iOS Application -->
@ -28,7 +31,7 @@
</head>
<body class="
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
{% if prefs.wide == "on" %} wide{% endif %}
{% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %} wide{% endif %}
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<!-- NAVIGATION BAR -->

View File

@ -24,7 +24,7 @@
{% if author.flair.flair_parts.len() > 0 %}
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
{% endif %}
<a href="{{ post_link }}{{ id }}/?context=3" class="created" title="{{ created }}">{{ rel_time }}</a>
<a href="{{ post_link }}{{ id }}/?context=3#{{ id }}" class="created" title="{{ created }}">{{ rel_time }}</a>
{% if edited.0 != "".to_string() %}<span class="edited" title="{{ edited.1 }}">edited {{ edited.0 }}</span>{% endif %}
{% if !awards.is_empty() && prefs.hide_awards != "on" %}
<span class="dot">&bull;</span>

View File

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

View File

@ -3,6 +3,10 @@
{% block title %}Redlib Settings{% endblock %}
{% block subscriptions %}
{% call utils::sub_list("") %}
{% endblock %}
{% block search %}
{% call utils::search("".to_owned(), "") %}
{% endblock %}
@ -37,13 +41,14 @@
<div class="prefs-group">
<label for="layout">Layout:</label>
<select name="layout" id="layout">
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
{% call utils::options(prefs.layout, ["card", "clean", "compact", "old", "waterfall"], "card") %}
</select>
</div>
<div class="prefs-group">
<label for="wide">Wide UI:</label>
<input type="hidden" value="off" name="wide">
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
</div>
</fieldset>
<fieldset>

View File

@ -27,7 +27,7 @@
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
{% endif %}
</div>
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
</select>
<button id="sort_submit" class="submit">
@ -134,7 +134,13 @@
</form>
{% endif %}
</div>
</div>
{% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
</div>
{% endif %}
</div>
</details>
<details class="panel" id="sidebar" open>

View File

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

View File

@ -64,7 +64,7 @@
{% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT -->
<div class="post highlighted">
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
<p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span>
@ -87,12 +87,12 @@
{% endif %}
</p>
<h1 class="post_title">
{{ post.title }}
{% if post.flair.flair_parts.len() > 0 %}
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
class="post_flair"
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %}
{{ post.title }}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h1>
@ -104,12 +104,11 @@
<a href="{{ post.media.url }}" class="post_media_image" >
{% if post.media.height == 0 || post.media.width == 0 %}
<!-- i.redd.it images special case -->
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"{%if post_should_be_blurred %} class="post_nsfw_blur"{% endif %}/>
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %}
<svg
width="{{ post.media.width }}px"
height="{{ post.media.height }}px"
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.media.url }}"/>
<desc>
@ -127,7 +126,7 @@
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<script src="/hls.min.js"></script>
<div class="post_media_content">
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
@ -135,7 +134,7 @@
<script src="/videoUtils.js"></script>
{% else %}
<div class="post_media_content">
<video class="post_media_video{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
</div>
{% call render_hls_notification(post.permalink[1..]) %}
{% endif %}
@ -168,13 +167,32 @@
<span class="label"> Upvotes</span></div>
<div class="post_footer">
<ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li>
<li>
<a href="{{ post.permalink }}">
<span class="desktop_item">perma</span>link
</a>
</li>
{% if post.num_duplicates > 0 %}
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
<li>
<a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
dup<span class="desktop_item">licat</span>es
</a>
</li>
{% endif %}
{% if post.post_type == "link" %}
<li class="desktop_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive.is</a></li>
<li class="mobile_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive</a></li>
{% endif %}
{% call external_reddit_link(post.permalink) %}
{% if post.media.download_name != "" %}
<li>
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
<span class="mobile_item">dl</span>
<span class="desktop_item">download</span>
</a>
</li>
{% endif %}
</ul>
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
</div>
@ -182,8 +200,7 @@
{%- endmacro %}
{% macro external_reddit_link(permalink) %}
{% for dev_type in ["desktop", "mobile"] %}
<li class="{{ dev_type }}_item">
<li>
<a
{% if prefs.disable_visit_reddit_confirmation != "on" %}
href="#popup"
@ -197,12 +214,11 @@
{% call visit_reddit_confirmation(permalink) %}
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% macro post_in_list(post) -%}
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
<p class="post_header">
{% let community -%}
{% if post.community.starts_with("u_") -%}
@ -233,7 +249,7 @@
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h2>
<!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
{% if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && post.post_type == "image" %}
<div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
{% if post.media.height == 0 || post.media.width == 0 %}
@ -241,7 +257,6 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %}
<svg
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px"
height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg">
@ -253,26 +268,22 @@
{% endif %}
</a>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
<div class="post_media_content">
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || post.post_type == "video") %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content">
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
</div>
{% else %}
<div class="post_media_content">
<video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
<video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %}
{% else if post.post_type != "self" %}
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
<a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
{% if post.thumbnail.url.is_empty() %}
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
<title>Thumbnail</title>
@ -280,7 +291,7 @@
</svg>
{% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post_should_be_blurred %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>