Compare commits

...

102 Commits

Author SHA1 Message Date
05e2c31bec #18
i seem to have left my brain somewhere
2025-01-13 15:10:35 +13:00
dcad2ac142 Merge pull request 'subreddit banners' (#18) from subreddit-banners into main
Reviewed-on: #18
2025-01-13 14:48:31 +13:00
0dcda02d27 Merge branch 'main' into subreddit-banners 2025-01-13 14:48:11 +13:00
7e09609daa Merge pull request 'subreddit-created-date' (#19) from subreddit-created-date into main
Reviewed-on: #19
2025-01-13 14:44:15 +13:00
49afd83ad3 subreddit-created-date 2025-01-13 14:41:49 +13:00
138172b365 unnecessary log generation 2025-01-13 14:18:05 +13:00
200509255c allow disabling of banner 2025-01-13 14:15:35 +13:00
bfcc946baa subreddit banners 2025-01-11 15:05:18 +13:00
0791f6af41 video quality recategorization 2025-01-09 08:56:51 +13:00
76bf796572 video quality recategorization 2025-01-09 08:56:33 +13:00
d7cff4203a v0.35.4 2025-01-07 07:28:00 +13:00
86cafe134e actually fixes #10 #11 2025-01-06 16:34:17 +13:00
fedd76877c add "dprksoldier" mascot (closes #15) 2025-01-06 16:03:50 +13:00
fcfd4dd196 Update README.md
fix screenshot without a broken grid
2025-01-06 15:41:22 +13:00
0081205ace fix details grid 2025-01-06 15:13:22 +13:00
482b528f49 video download style change 2025-01-06 14:38:17 +13:00
b6d828727c Update README.md 2025-01-06 13:09:39 +13:00
fe9128a5e2 Merge branch 'quicklist' 2025-01-06 12:44:33 +13:00
836cb15946 minimum viable product 2025-01-06 12:33:49 +13:00
3a5abd6c9d shutup warning 2025-01-06 12:01:48 +13:00
fd0b0968dd Merge pull request 'style-gimmicks' (#13) from style-gimmicks into main
Reviewed-on: #13
2025-01-06 09:42:14 +13:00
e94d07274d gimmick for "style-gimmicks" 2025-01-06 09:38:08 +13:00
85dab5e070 add quicklist to subreddit 2025-01-06 09:32:52 +13:00
5959464bbe fix typo, lol.. 2025-01-06 08:04:41 +13:00
dbb13f73f4 icon change 2025-01-06 07:59:03 +13:00
10c7327c39 colorway toggle 2025-01-06 07:15:24 +13:00
ad39254ddc fix followed user rendering 2024-12-22 04:25:54 +13:00
9856b7fd47 create basic quicklist functionality 2024-12-22 04:04:43 +13:00
e478575706 Add mascot: BoymoderHoodie 2024-12-22 04:02:54 +13:00
dc84d9b503 unfinished checkupdate changes (needs fixing(soon)) 2024-11-21 00:58:15 +13:00
7afc0d006c Merge remote-tracking branch 'upstream/main' 2024-11-21 00:53:33 +13:00
d3ba5f3efb feat(error): add new instance buttom 2024-11-19 16:30:37 -05:00
cb9a2a3c39 fix(client): revert to hyper_rustls :P hi SWE 👋 2024-11-19 15:48:42 -05:00
6ecdedd2ed feat(client): additionally randomize headers 2024-11-19 14:54:06 -05:00
18efb8c714 fix(client): update headers 2024-11-19 14:10:59 -05:00
0bc36d529c Add Quadlet Container File (#319)
* Add Quadlet Container File

* Update README.md with Quadlet instructions
2024-11-19 13:19:48 -05:00
96ebfd2d3a fix(ci): statically build on artifacts 2024-11-19 12:53:36 -05:00
3e1718bfc9 fix(client): ??? no accept language 2024-11-19 12:44:20 -05:00
96e40e8887 style(clippy): small clippy change 2024-11-19 11:40:17 -05:00
f8a9ad363d chore(deps): updates 2024-11-19 11:37:30 -05:00
f7240208f1 fix(tls): vendor native-tls 2024-11-19 11:18:20 -05:00
0714d58efe fix(ci): install new openssl requirements 2024-11-19 11:12:04 -05:00
a96bebb099 fix(client): switch to hyper-tls 2024-11-19 11:08:00 -05:00
6c64ebd56b fix(scraper): additionally grab common words 2024-11-15 16:53:00 -05:00
62717ef6b2 fix: update error template 2024-11-14 11:49:47 -05:00
a301afc383 fix(scraper): truncate to post count 2024-11-13 16:43:41 -05:00
6a18ea17ec Use quotes for kaniko to expand ARG in Dockerfile (#314) 2024-11-10 20:19:40 -05:00
feedc572cd change bin name 2024-11-03 10:36:46 +13:00
a3bc16f7d2 port update checker to gitea compatible version 2024-11-03 10:08:26 +13:00
bd4cb96c0f "fix" scraper 2024-11-03 09:36:11 +13:00
a9c99cc752 Merge remote-tracking branch 'upstream/main' 2024-11-03 09:34:12 +13:00
f03bdcf472 feat: display whether or not the instance is up to date on error (#310) 2024-11-01 18:16:25 -04:00
2fd358f3ed feat(hls): add video quality preference (#306) 2024-11-01 12:28:52 -04:00
5ef57812f8 style: fix clippy 2024-11-01 11:39:05 -04:00
d17d097b12 Fix parts of CI (#304)
* Run cargo fmt, hide clippy::cmp_owned errors

* Bump deps

* Fix failing test

* Update src/client.rs

---------

Co-authored-by: Matthew Esposito <matt@matthew.science>
2024-10-31 22:50:50 -04:00
a96894c743 enables http2 crate feature, replaces http1 protocol with http2 on co… (#305) 2024-10-31 22:48:19 -04:00
9aea9c90a2 fix: reduce to minimum patch, fix clippy 2024-10-31 16:09:35 -04:00
efdf1848ac fix: emergency patch for 403 2024-10-31 16:06:29 -04:00
bc9530821d feat(scraper): add output file 2024-10-30 15:15:38 -04:00
f3d2f0cc59 feat(scraper): add scraper CLI 2024-10-21 20:54:05 -04:00
49ef59e000 chore: make library 2024-10-21 20:46:03 -04:00
6773e3756b Update README.md 2024-10-20 21:16:03 +13:00
1c1e627815 v0.35.3 2024-10-20 19:31:07 +13:00
4b854eb84d fix/change feed list alignment 2024-10-20 19:20:01 +13:00
712790acbe fix footer spacing 2024-10-20 18:08:26 +13:00
062d810aad settings rearrangement 2024-10-20 18:06:26 +13:00
64eec64ebe Merge remote-tracking branch 'upstream/main' 2024-10-20 17:32:13 +13:00
3ff907d6c1 additional new colour tweaks (#285) 2024-10-12 11:42:15 -04:00
f4a457e529 Add additional themes to README (#284) 2024-10-11 15:43:45 -04:00
7dda8d9bbb use better accent colour + add libreddit styles (#281)
* update favicon with new logo

* only have 32x32 .ico file

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

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

* fix apple-touch-icon.png size

* remove width, length

---------

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

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

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

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

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

* Make (most) emotes embed within comments

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

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

---------

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

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

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

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

Updated digest name on upload (as per 1187084)

* Update main-docker.yml (digests download)

Trying another fix based on the template provided here https://github.com/actions/download-artifact for downloading multiple (filtered) Artifacts to the same directory
2024-09-17 14:37:39 -04:00
f18d135045 Update main-docker.yml (digests) (#238)
Updated digest name on upload (as per 1187084)
2024-09-17 14:09:32 -04:00
43 changed files with 1898 additions and 782 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target

View File

@ -16,6 +16,8 @@ REDLIB_PUSHSHIFT_FRONTEND=undelete.pullpush.io
REDLIB_DEFAULT_THEME=system REDLIB_DEFAULT_THEME=system
# Set the default mascot # Set the default mascot
REDLIB_DEFAULT_MASCOT=none REDLIB_DEFAULT_MASCOT=none
# Enable showing redsunlib colorway by default
REDLIB_DEFAULT_REDSUNLIB_COLORWAY=off
# Set the default front page (options: default, popular, all) # Set the default front page (options: default, popular, all)
REDLIB_DEFAULT_FRONT_PAGE=default REDLIB_DEFAULT_FRONT_PAGE=default
# Set the default layout (options: card, clean, compact) # Set the default layout (options: card, clean, compact)

5
.github/FUNDING.yml vendored
View File

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

View File

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

View File

@ -15,15 +15,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- { platform: linux/amd64, target: x86_64-unknown-linux-musl} - { platform: linux/amd64, target: x86_64-unknown-linux-musl }
- { platform: linux/arm64, target: aarch64-unknown-linux-musl} - { platform: linux/arm64, target: aarch64-unknown-linux-musl }
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf} - { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf }
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- - name: Docker meta
name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
@ -31,21 +29,17 @@ jobs:
tags: | tags: |
type=sha type=sha
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- - name: Set up QEMU
name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - name: Login to Quay.io Container Registry
name: Login to Quay.io Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }} password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- - name: Build and push
name: Build and push
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
@ -55,14 +49,12 @@ 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@v4
with: with:
name: digests-${{ matrix.target }} name: digests-${{ matrix.target }}
@ -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@v4.1.7
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 }}

1333
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,35 +3,36 @@ name = "redsunlib"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib" repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.2" version = "0.35.4"
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>",
] ]
edition = "2021" edition = "2021"
default-run = "redsunlib"
[dependencies] [dependencies]
askama = { version = "0.12.1", default-features = false } rinja = { version = "0.3.4", default-features = false }
cached = { version = "0.51.3", features = ["async"] } cached = { version = "0.54.0", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [ clap = { version = "4.4.11", default-features = false, features = [
"std", "std",
"env", "env",
"derive",
] } ] }
regex = "1.10.2" regex = "1.10.2"
serde = { version = "1.0.193", features = ["derive"] } 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.31", features = ["full"] }
hyper-rustls = "0.25.0"
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.133"
tokio = { version = "1.35.1", features = ["full"] } tokio = { version = "1.35.1", features = ["full"] }
time = { version = "0.3.31", features = ["local-offset"] } time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0" url = "2.5.0"
rust-embed = { version = "8.1.0", features = ["include-exclude"] } rust-embed = { version = "8.1.0", features = ["include-exclude"] }
libflate = "2.0.0" libflate = "2.0.0"
brotli = { version = "6.0.0", features = ["std"] } brotli = { version = "7.0.0", features = ["std"] }
toml = "0.8.8" toml = "0.8.8"
once_cell = "1.19.0" once_cell = "1.19.0"
serde_yaml = "0.9.29" serde_yaml = "0.9.29"
@ -44,6 +45,10 @@ pretty_env_logger = "0.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
rss = "2.0.7" rss = "2.0.7"
arc-swap = "1.7.1" arc-swap = "1.7.1"
serde_json_path = "0.7.1"
async-recursion = "1.1.1"
common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] }
hyper-rustls = { version = "0.24.2", features = [ "http2" ] }
[dev-dependencies] [dev-dependencies]
@ -54,3 +59,11 @@ sealed_test = "1.0.0"
codegen-units = 1 codegen-units = 1
lto = true lto = true
strip = "symbols" strip = "symbols"
[[bin]]
name = "redsunlib"
path = "src/main.rs"
[[bin]]
name = "scraper"
path = "src/scraper/main.rs"

View File

@ -5,7 +5,7 @@
<br> <br>
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20) ![screenshot](https://git.stardust.wtf/attachments/ccf81f52-e653-4722-94b9-b370c58d6359)
### Disclaimer ### Disclaimer
@ -64,14 +64,13 @@ Redlib currently implements most of Reddit's (signed-out) functionalities but st
**Red sun** in the sky + Red**lib** = Redsunlib **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 :/ <sup>I do self criticism constantly, because I'm trapped in a Maoist *cult* where comrades (white terrorists) criticize me merciloussly for having a fascist credit card (VISA Silver Signature Rewards) They won't let me order vegan pizza anymore because the phone is fascist and "summoning my pizza slave with bourgeois app" is "bad vibes"</sup>
## Built with ## Built with
- [Rust](https://www.rust-lang.org/) - Programming language - [Rust](https://www.rust-lang.org/) - Programming language
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client - [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
- [Askama](https://github.com/djc/askama) - Templating engine - [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
- [Rustls](https://github.com/rustls/rustls) - TLS library - [Rustls](https://github.com/rustls/rustls) - TLS library
## How is it different from other Reddit front ends? ## How is it different from other Reddit front ends?
@ -320,7 +319,7 @@ 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", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ | | `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` | | `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` | | `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
@ -339,4 +338,5 @@ Assign a default value for each user-modifiable setting by passing environment v
| `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` | | `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
| `HIDE_BANNER` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` | | `FIXED_NAVBAR` | `["on", "off"]` | `on` |

View File

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

View File

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

16
redlib.container Normal file
View File

@ -0,0 +1,16 @@
[Install]
WantedBy=default.target
[Container]
AutoUpdate=registry
ContainerName=redlib
DropCapability=ALL
EnvironmentFile=.env
HealthCmd=["wget","--spider","-q","--tries=1","http://localhost:8080/settings"]
HealthInterval=5m
HealthTimeout=3s
Image=quay.io/redlib/redlib:latest
NoNewPrivileges=true
PublishPort=8080:8080
ReadOnly=true
User=nobody

View File

@ -4,11 +4,10 @@ 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::header::HeaderValue;
use hyper::StatusCode; use hyper::{body, body::Buf, 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, trace, warn}; use log::{error, debug, 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;
@ -23,17 +22,18 @@ 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"; const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
let https = hyper_rustls::HttpsConnectorBuilder::new() const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
.with_native_roots()
.expect("No native root certificates found") const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
.https_only() const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
.enable_http1()
.build(); pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
client::Client::builder().build(https) Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
});
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| { pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new()); let client = block_on(Oauth::new());
@ -45,6 +45,11 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false); pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
static URL_PAIRS: [(&str, &str); 2] = [
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
];
/// Gets the canonical path for a resource on Reddit. This is accomplished by /// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`. /// making a `HEAD` request to Reddit at the path given in `path`.
/// ///
@ -58,8 +63,28 @@ pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
/// `Location` header. An `Err(String)` is returned if Reddit responds with a /// `Location` header. An `Err(String)` is returned if Reddit responds with a
/// 429, or if we were unable to decode the value in the `Location` header. /// 429, or if we were unable to decode the value in the `Location` header.
#[cached(size = 1024, time = 600, result = true)] #[cached(size = 1024, time = 600, result = true)]
pub async fn canonical_path(path: String) -> Result<Option<String>, String> { #[async_recursion::async_recursion]
let res = reddit_head(path.clone(), true).await?; pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
if tries == 0 {
return Ok(None);
}
// for each URL pair, try the HEAD request
let res = {
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
let mut res = None;
for (url_base, url_base_host) in URL_PAIRS {
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
if let Some(res) = &res {
if !res.status().is_client_error() {
break;
}
}
}
res
};
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
let status = res.status().as_u16(); let status = res.status().as_u16();
let policy_error = res.headers().get(header::RETRY_AFTER).is_some(); let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
@ -73,6 +98,7 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
let Ok(original) = val.to_str() else { let Ok(original) = val.to_str() else {
return Err("Unable to decode Location header.".to_string()); return Err("Unable to decode Location header.".to_string());
}; };
// We need to strip the .json suffix from the original path. // We need to strip the .json suffix from the original path.
// In addition, we want to remove share parameters. // In addition, we want to remove share parameters.
// Cut it off here instead of letting it propagate all the way // Cut it off here instead of letting it propagate all the way
@ -85,7 +111,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// also remove all Reddit domain parts with format_url. // also remove all Reddit domain parts with format_url.
// Otherwise, it will literally redirect to Reddit.com. // Otherwise, it will literally redirect to Reddit.com.
let uri = format_url(stripped_uri); let uri = format_url(stripped_uri);
Ok(Some(uri))
// Decrement tries and try again
canonical_path(uri, tries - 1).await
} }
None => Ok(None), None => Ok(None),
}, },
@ -126,7 +154,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?; let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Build the hyper client from the HTTPS connector. // Build the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone(); let client: &Lazy<Client<_, Body>> = &CLIENT;
let mut builder = Request::get(parsed_uri); let mut builder = Request::get(parsed_uri);
@ -166,23 +194,29 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP /// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
/// 3xx codes Reddit returns and will automatically redirect. /// 3xx codes Reddit returns and will automatically redirect.
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
request(&Method::GET, path, true, quarantine) request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
} }
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects. /// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
request(&Method::HEAD, path, false, quarantine) request(&Method::HEAD, path, false, quarantine, base_path, host)
} }
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
// request(&Method::HEAD, path, false, quarantine, false)
// }
// Unused - reddit_head is only ever called in the context of a short URL
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect` /// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
/// will recurse on the URL that Reddit provides in the Location HTTP header /// will recurse on the URL that Reddit provides in the Location HTTP header
/// in its response. /// in its response.
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> { fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
// Build Reddit URL from path. // Build Reddit URL from path.
let url = format!("{REDDIT_URL_BASE}{path}"); let url = format!("{base_path}{path}");
// Construct the hyper client from the HTTPS connector. // Construct the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone(); let client: &Lazy<Client<_, Body>> = &CLIENT;
let (token, vendor_id, device_id, user_agent, loid) = { let (token, vendor_id, device_id, user_agent, loid) = {
let client = OAUTH_CLIENT.load_full(); let client = OAUTH_CLIENT.load_full();
@ -197,27 +231,37 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// 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 mut headers = vec![
.method(method) ("User-Agent", user_agent),
.uri(&url) ("Client-Vendor-Id", vendor_id),
.header("User-Agent", user_agent) ("X-Reddit-Device-Id", device_id),
.header("Client-Vendor-Id", vendor_id) ("x-reddit-loid", loid),
.header("X-Reddit-Device-Id", device_id) ("Host", host.to_string()),
.header("x-reddit-loid", loid) ("Authorization", format!("Bearer {token}")),
.header("Host", "oauth.reddit.com") ("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }),
.header("Authorization", &format!("Bearer {token}")) (
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive")
.header(
"Cookie", "Cookie",
if quarantine { if quarantine {
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D" "_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D".into()
} else { } else {
"" "".into()
}, },
) ),
.body(Body::empty()); ("X-Reddit-Width", fastrand::u32(300..500).to_string()),
("X-Reddit-DPR", "2".to_owned()),
("Device-Name", format!("Android {}", fastrand::u8(9..=14))),
];
// shuffle headers: https://github.com/redlib-org/redlib/issues/324
fastrand::shuffle(&mut headers);
let mut builder = Request::builder().method(method).uri(&url);
for (key, value) in headers {
builder = builder.header(key, value);
}
let builder = builder.body(Body::empty());
async move { async move {
match builder { match builder {
@ -259,16 +303,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.to_string(), .to_string(),
true, true,
quarantine, quarantine,
base_path,
host,
) )
.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),
@ -356,7 +396,7 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
response.headers().get("x-ratelimit-reset").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())), response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
) { ) {
trace!( debug!(
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}", "Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
if is_rolling_over { "yes" } else { "no" }, if is_rolling_over { "yes" } else { "no" },
); );
@ -386,6 +426,16 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
match serde_json::from_reader(body.reader()) { match serde_json::from_reader(body.reader()) {
Ok(value) => { Ok(value) => {
let json: Value = value; let json: Value = value;
// If user is suspended
if let Some(data) = json.get("data") {
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
if is_suspended {
return Err("suspended".into());
}
}
}
// If Reddit returned an error // If Reddit returned an error
if json["error"].is_i64() { if json["error"].is_i64() {
// OAuth token has expired; http status 401 // OAuth token has expired; http status 401
@ -394,6 +444,7 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await; let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string()); return Err("OAuth token has expired. Please refresh the page!".to_string());
} }
// Handle quarantined // Handle quarantined
if json["reason"] == "quarantined" { if json["reason"] == "quarantined" {
return Err("quarantined".into()); return Err("quarantined".into());
@ -402,6 +453,15 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
if json["reason"] == "gated" { if json["reason"] == "gated" {
return Err("gated".into()); return Err("gated".into());
} }
// Handle private subs
if json["reason"] == "private" {
return Err("private".into());
}
// Handle banned subs
if json["reason"] == "banned" {
return Err("banned".into());
}
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"])) Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else { } else {
Ok(json) Ok(json)
@ -437,13 +497,34 @@ async fn test_localization_popular() {
async fn test_obfuscated_share_link() { async fn test_obfuscated_share_link() {
let share_link = "/r/rust/s/kPgq8WNHRK".into(); let share_link = "/r/rust/s/kPgq8WNHRK".into();
// Correct link without share parameters // Correct link without share parameters
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into(); let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link))); assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_share_link_strip_json() { async fn test_share_link_strip_json() {
let link = "/17krzvz".into(); let link = "/17krzvz".into();
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into(); let canonical_link = "/comments/17krzvz".into();
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link))); assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_private_sub() {
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("private".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_banned_sub() {
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("banned".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_gated_sub() {
// quarantine to false to specifically catch when we _don't_ catch it
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
assert!(link.is_err());
assert_eq!(link, Err("gated".into()));
} }

View File

@ -84,6 +84,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
pub(crate) default_hide_sidebar_and_summary: Option<String>, pub(crate) default_hide_sidebar_and_summary: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_BANNER")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_BANNER")]
pub(crate) default_hide_banner: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")] #[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
pub(crate) default_hide_score: Option<String>, pub(crate) default_hide_score: Option<String>,
@ -96,6 +100,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")] #[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>, pub(crate) default_filters: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_QUICKLIST")]
#[serde(alias = "LIBREDDIT_DEFAULT_QUICKLIST")]
pub(crate) default_quicklist: 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>,
@ -157,9 +165,11 @@ impl Config {
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"), default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"),
default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"), default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"),
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_banner: parse("REDLIB_DEFAULT_HIDE_BANNER"),
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_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_quicklist: parse("REDLIB_DEFAULT_QUICKLIST"),
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"),
@ -188,9 +198,11 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_DEFAULT_WIDE" => config.default_wide.clone(), "REDLIB_DEFAULT_WIDE" => config.default_wide.clone(),
"REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(), "REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
"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_BANNER" => config.default_hide_banner.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_FILTERS" => config.default_filters.clone(),
"REDLIB_DEFAULT_QUICKLIST" => config.default_quicklist.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(),
@ -271,6 +283,12 @@ fn test_default_filters() {
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into())); assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
} }
#[test]
#[sealed_test(env = [("REDLIB_DEFAULT_QUICKLIST", "news+popular")])]
fn test_default_quicklist() {
assert_eq!(get_setting("REDLIB_DEFAULT_QUICKLIST"), Some("news+popular".into()));
}
#[test] #[test]
#[sealed_test] #[sealed_test]
fn test_pushshift() { fn test_pushshift() {

View File

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

View File

@ -3,10 +3,10 @@ use crate::{
server::RequestExt, server::RequestExt,
utils::{ErrorTemplate, Preferences}, utils::{ErrorTemplate, Preferences},
}; };
use askama::Template;
use build_html::{Container, Html, HtmlContainer, Table}; use build_html::{Container, Html, HtmlContainer, Table};
use hyper::{http::Error, Body, Request, Response}; use hyper::{http::Error, Body, Request, Response};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rinja::Template;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -85,7 +85,7 @@ fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
pub struct InstanceInfo { pub struct InstanceInfo {
package_name: String, package_name: String,
crate_version: String, crate_version: String,
git_commit: String, pub git_commit: String,
deploy_date: String, deploy_date: String,
compile_mode: String, compile_mode: String,
deploy_unix_ts: i64, deploy_unix_ts: i64,
@ -152,6 +152,7 @@ impl InstanceInfo {
["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)], ["Filters", &convert(&self.config.default_filters)],
["Quick Access Feeds", &convert(&self.config.default_quicklist)],
]) ])
.with_header_row(["Default preferences"]), .with_header_row(["Default preferences"]),
); );
@ -189,7 +190,8 @@ impl InstanceInfo {
Default use FFmpeg: {:?}\n Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n Default subscriptions: {:?}\n
Default filters: {:?}\n", Default filters: {:?}\n
Default quicklist: {:?}\n",
self.package_name, self.package_name,
self.crate_version, self.crate_version,
self.git_commit, self.git_commit,
@ -218,6 +220,7 @@ impl InstanceInfo {
self.config.default_hide_hls_notification, self.config.default_hide_hls_notification,
self.config.default_subscriptions, self.config.default_subscriptions,
self.config.default_filters, self.config.default_filters,
self.config.default_quicklist,
) )
} }
StringType::Html => self.to_table(), StringType::Html => self.to_table(),

13
src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
pub mod client;
pub mod config;
pub mod duplicates;
pub mod instance_info;
pub mod oauth;
pub mod oauth_resources;
pub mod post;
pub mod search;
pub mod server;
pub mod settings;
pub mod subreddit;
pub mod user;
pub mod utils;

View File

@ -2,35 +2,21 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![allow(clippy::cmp_owned)] #![allow(clippy::cmp_owned)]
// Reference local files use cached::proc_macro::cached;
mod config;
mod duplicates;
mod instance_info;
mod oauth;
mod oauth_resources;
mod post;
mod search;
mod settings;
mod subreddit;
mod user;
mod utils;
// Import Crates
use clap::{Arg, ArgAction, Command}; use clap::{Arg, ArgAction, Command};
use std::str::FromStr;
use futures_lite::FutureExt; use futures_lite::FutureExt;
use hyper::Uri;
use hyper::{header::HeaderValue, Body, Request, Response}; use hyper::{header::HeaderValue, Body, Request, Response};
mod client;
use client::{canonical_path, proxy};
use log::info; use log::info;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use server::RequestExt; use redsunlib::client::{canonical_path, proxy, CLIENT};
use utils::{error, redirect, ThemeAssets, MascotAssets}; use redsunlib::server::{self, RequestExt};
use redsunlib::utils::{error, redirect, ThemeAssets, MascotAssets};
use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user};
use crate::client::OAUTH_CLIENT; use redsunlib::client::OAUTH_CLIENT;
mod server;
// Create Services // Create Services
@ -257,6 +243,13 @@ async fn main() {
app app
.at("/highlighted.js") .at("/highlighted.js")
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed()); .get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
app
.at("/check_update.js")
.get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed());
app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed());
app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed());
// FFmpeg // FFmpeg
app app
.at("/ffmpeg/814.ffmpeg.js") .at("/ffmpeg/814.ffmpeg.js")
@ -284,6 +277,9 @@ async fn main() {
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed()); app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed()); app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed()); app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
app
.at("/emote/:subreddit_id/:filename")
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
app app
.at("/preview/:loc/award_images/:fullname/:id") .at("/preview/:loc/award_images/:fullname/:id")
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed()); .get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
@ -327,10 +323,12 @@ async fn main() {
.at("/r/u_:name") .at("/r/u_:name")
.get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed()); .get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters(r).boxed()); app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed()); app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/filter").post(|r| subreddit::subscriptions_filters(r).boxed()); app.at("/r/:sub/filter").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unfilter").post(|r| subreddit::subscriptions_filters(r).boxed()); app.at("/r/:sub/unfilter").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/quicklist").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unquicklist").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed()); app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed());
app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed()); app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed());
@ -389,7 +387,7 @@ async fn main() {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
match req.param("id").as_deref() { match req.param("id").as_deref() {
// Share link // Share link
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).await { Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
Ok(Some(path)) => Ok(redirect(&path)), Ok(Some(path)) => Ok(redirect(&path)),
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
Err(e) => error(req, &e).await, Err(e) => error(req, &e).await,
@ -408,7 +406,7 @@ async fn main() {
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
// Short link for post // Short link for post
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}")).await { Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
Ok(path_opt) => match path_opt { Ok(path_opt) => match path_opt {
Some(path) => Ok(redirect(&path)), Some(path) => Ok(redirect(&path)),
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
@ -434,3 +432,41 @@ async fn main() {
eprintln!("Server error: {e}"); eprintln!("Server error: {e}");
} }
} }
pub async fn proxy_commit_info() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/atom+xml")
.body(Body::from(fetch_commit_info().await))
.unwrap_or_default(),
)
}
#[cached(time = 600)]
async fn fetch_commit_info() -> String {
let uri = Uri::from_str("https://git.stardust.wtf/api/v1/repos/iridium/redsunlib/commits?verification=false&stat=false").expect("Invalid URI");
let resp: Body = CLIENT.get(uri).await.expect("Failed to request git.stardust.wtf").into_body();
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
}
pub async fn proxy_instances() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/json")
.body(Body::from(fetch_instances().await))
.unwrap_or_default(),
)
}
#[cached(time = 600)]
async fn fetch_instances() -> String {
let uri = Uri::from_str("https://raw.githubusercontent.com/redlib-org/redlib-instances/refs/heads/main/instances.json").expect("Invalid URI");
let resp: Body = CLIENT.get(uri).await.expect("Failed to request GitHub").into_body();
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
}

View File

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

View File

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

132
src/scraper/main.rs Normal file
View File

@ -0,0 +1,132 @@
use std::{collections::HashMap, fmt::Display, io::Write};
use clap::{Parser, ValueEnum};
use common_words_all::{get_top, Language, NgramSize};
use redsunlib::utils::Post;
#[derive(Parser)]
#[command(name = "my_cli")]
#[command(about = "A simple CLI example", long_about = None)]
struct Cli {
#[arg(short = 's', long = "sub")]
sub: String,
#[arg(long = "sort")]
sort: SortOrder,
#[arg(short = 'f', long = "format", value_enum)]
format: Format,
#[arg(short = 'o', long = "output")]
output: Option<String>,
}
#[derive(Debug, Clone, ValueEnum)]
enum SortOrder {
Hot,
Rising,
New,
Top,
Controversial,
}
impl Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortOrder::Hot => write!(f, "hot"),
SortOrder::Rising => write!(f, "rising"),
SortOrder::New => write!(f, "new"),
SortOrder::Top => write!(f, "top"),
SortOrder::Controversial => write!(f, "controversial"),
}
}
}
#[derive(Debug, Clone, ValueEnum)]
enum Format {
Json,
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let cli = Cli::parse();
let (sub, sort, format, output) = (cli.sub, cli.sort, cli.format, cli.output);
let initial = format!("/r/{sub}/{sort}.json?&raw_json=1");
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
let mut hashmap = HashMap::new();
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
loop {
print!("\r");
let path = format!("/r/{sub}/{sort}.json?sort={sort}&t=&after={after}&raw_json=1");
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
let old_len = hashmap.len();
// convert to hashmap and extend hashmap
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
let len = new_posts.len();
hashmap.extend(new_posts);
if hashmap.len() - old_len < 3 {
break;
}
let x = hashmap.len() - old_len;
after = new_after;
// Print number of posts fetched
print!("Fetched {len} posts (+{x})",);
std::io::stdout().flush().unwrap();
}
println!("\n\n");
// additionally search if final count not reached
for word in get_top(Language::English, 10_000, NgramSize::One) {
let mut retrieved_posts_from_search = 0;
let initial = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}");
println!("Grabbing posts with word {word}.");
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
'search: loop {
let path = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}&after={after}");
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
if new_posts.is_empty() || new_after.is_empty() {
println!("No more posts for word {word}");
break 'search;
}
retrieved_posts_from_search += new_posts.len();
let old_len = hashmap.len();
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
let len = new_posts.len();
hashmap.extend(new_posts);
let delta = hashmap.len() - old_len;
after = new_after;
// Print number of posts fetched
println!("Fetched {len} posts (+{delta})",);
if retrieved_posts_from_search > 1000 {
println!("Reached 1000 posts from search");
break 'search;
}
}
// Need to save incrementally. atomic save + move
let tmp_file = output.clone().unwrap_or_else(|| format!("{sub}.json.tmp"));
let perm_file = output.clone().unwrap_or_else(|| format!("{sub}.json"));
write_posts(&hashmap.values().collect(), tmp_file.clone());
// move file
std::fs::rename(tmp_file, perm_file).unwrap();
}
println!("\n\n");
println!("Size of hashmap: {}", hashmap.len());
let posts: Vec<&Post> = hashmap.values().collect();
match format {
Format::Json => {
let filename: String = output.unwrap_or_else(|| format!("{sub}.json"));
write_posts(&posts, filename);
}
}
}
fn write_posts(posts: &Vec<&Post>, filename: String) {
let json = serde_json::to_string(&posts).unwrap();
std::fs::write(filename, json).unwrap();
}

View File

@ -1,14 +1,16 @@
#![allow(clippy::cmp_owned)]
// CRATES // CRATES
use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences}; use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
use crate::{ use crate::{
client::json, client::json,
server::RequestExt,
subreddit::{can_access_quarantine, quarantine}, subreddit::{can_access_quarantine, quarantine},
RequestExt,
}; };
use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rinja::Template;
// STRUCTS // STRUCTS
struct SearchParams { struct SearchParams {
@ -60,7 +62,8 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
} else { } else {
"" ""
}; };
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); let uri_path = req.uri().path().replace("+", "%2B");
let path = format!("{}.json?{}{}&raw_json=1", uri_path, req.uri().query().unwrap_or_default(), nsfw_results);
let mut query = param(&path, "q").unwrap_or_default(); let mut query = param(&path, "q").unwrap_or_default();
query = REDDIT_URL_MATCH.replace(&query, "").to_string(); query = REDDIT_URL_MATCH.replace(&query, "").to_string();
@ -68,10 +71,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
return Ok(redirect("/")); return Ok(redirect("/"));
} }
if query.starts_with("r/") { if query.starts_with("r/") || query.starts_with("user/") {
return Ok(redirect(&format!("/{query}"))); return Ok(redirect(&format!("/{query}")));
} }
if query.starts_with("u/") {
return Ok(redirect(&format!("/user{}", &query[1..])));
}
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let quarantined = can_access_quarantine(&req, &sub); let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits // Handle random subreddits

View File

@ -1,4 +1,5 @@
#![allow(dead_code)] #![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use brotli::enc::{BrotliCompress, BrotliEncoderParams}; use brotli::enc::{BrotliCompress, BrotliEncoderParams};
use cached::proc_macro::cached; use cached::proc_macro::cached;
@ -195,6 +196,12 @@ impl Route<'_> {
} }
} }
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
impl Server { impl Server {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -723,7 +730,7 @@ mod tests {
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())), CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
_ => panic!("no decompressor for {}", expected_encoding.to_string()), _ => panic!("no decompressor for {}", expected_encoding),
}; };
let mut decompressed = Vec::<u8>::new(); let mut decompressed = Vec::<u8>::new();

View File

@ -1,12 +1,14 @@
#![allow(clippy::cmp_owned)]
use std::collections::HashMap; use std::collections::HashMap;
// CRATES // CRATES
use crate::server::ResponseExt; use crate::server::ResponseExt;
use crate::utils::{redirect, template, Preferences}; use crate::utils::{redirect, template, Preferences};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use futures_lite::StreamExt; use futures_lite::StreamExt;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
// STRUCTS // STRUCTS
@ -19,9 +21,10 @@ struct SettingsTemplate {
// CONSTANTS // CONSTANTS
const PREFS: [&str; 19] = [ const PREFS: [&str; 22] = [
"theme", "theme",
"mascot", "mascot",
"redsunlib_colorway",
"front_page", "front_page",
"layout", "layout",
"wide", "wide",
@ -35,10 +38,12 @@ const PREFS: [&str; 19] = [
"hide_hls_notification", "hide_hls_notification",
"autoplay_videos", "autoplay_videos",
"hide_sidebar_and_summary", "hide_sidebar_and_summary",
"hide_banner",
"fixed_navbar", "fixed_navbar",
"hide_awards", "hide_awards",
"hide_score", "hide_score",
"disable_visit_reddit_confirmation", "disable_visit_reddit_confirmation",
"video_quality",
]; ];
// FUNCTIONS // FUNCTIONS
@ -118,7 +123,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
let mut response = redirect(&path); let mut response = redirect(&path);
for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() { for name in [PREFS.to_vec(), vec!["subscriptions", "filters", "quicklist"]].concat() {
match form.get(name) { match form.get(name) {
Some(value) => response.insert_cookie( Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone())) Cookie::build((name.to_owned(), value.clone()))

View File

@ -1,16 +1,20 @@
#![allow(clippy::cmp_owned)]
use crate::{config, utils}; use crate::{config, utils};
// CRATES // CRATES
use crate::utils::{ use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
}; };
use crate::{client::json, server::ResponseExt, RequestExt}; use crate::{client::json, server::RequestExt, server::ResponseExt};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime,macros::format_description};
use log::trace;
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
@ -209,8 +213,8 @@ pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default() setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
} }
// Sub, filter, unfilter, or unsub by setting subscription cookie using response "Set-Cookie" header // Sub, filter, unfilter, quicklist, unquicklist or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn subscriptions_filters_quicklists(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect(); let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
@ -227,6 +231,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
let preferences = Preferences::new(&req); let preferences = Preferences::new(&req);
let mut sub_list = preferences.subscriptions; let mut sub_list = preferences.subscriptions;
let mut filters = preferences.filters; let mut filters = preferences.filters;
let mut quicklist = preferences.quicklist;
// Retrieve list of posts for these subreddits to extract display names // Retrieve list of posts for these subreddits to extract display names
@ -288,7 +293,16 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
} else if action.contains(&"unfilter".to_string()) { } else if action.contains(&"unfilter".to_string()) {
// Remove sub name from filtered list // Remove sub name from filtered list
filters.retain(|s| s.to_lowercase() != part.to_lowercase()); filters.retain(|s| s.to_lowercase() != part.to_lowercase());
} } else if action.contains(&"quicklist".to_string()) && !quicklist.contains(&part.to_owned()) {
// Add each sub name to the filtered list
quicklist.push(part.to_owned());
// Reorder quicklist alphabetically
quicklist.sort_by_key(|a| a.to_lowercase());
} else if action.contains(&"unquicklist".to_string()) {
// Remove sub name from filtered list
quicklist.retain(|s| s.to_lowercase() != part.to_lowercase());
}
} }
// Redirect back to subreddit // Redirect back to subreddit
@ -324,6 +338,17 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
.into(), .into(),
); );
} }
if quicklist.is_empty() {
response.remove_cookie("quicklist".to_string());
} else {
response.insert_cookie(
Cookie::build(("quicklist", quicklist.join("+")))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);
}
Ok(response) Ok(response)
} }
@ -438,14 +463,24 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
// Send a request to the url // Send a request to the url
let res = json(path, quarantined).await?; let res = json(path, quarantined).await?;
trace!("Subreddit info from r/{} : {}",sub,res["data"]);
// Metadata regarding the subreddit // Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64; let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64; let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
// Grab creation date as unix timestamp
let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH);
// Fetch subreddit icon either from the community_icon or icon_img value // Fetch subreddit icon either from the community_icon or icon_img value
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default(); let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() }; let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
// Fetch subreddit banner either from the banner_background_image or banner_img value
let banner_background_image: &str = res["data"]["banner_background_image"].as_str().unwrap_or_default();
let banner = if banner_background_image.is_empty() { val(&res, "banner_img") } else { banner_background_image.to_string() };
Ok(Subreddit { Ok(Subreddit {
name: val(&res, "display_name"), name: val(&res, "display_name"),
title: val(&res, "title"), title: val(&res, "title"),
@ -453,8 +488,10 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
info: rewrite_urls(&val(&res, "description_html")), info: rewrite_urls(&val(&res, "description_html")),
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(), // moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
icon: format_url(&icon), icon: format_url(&icon),
banner: format_url(&banner),
members: format_num(members), members: format_num(members),
active: format_num(active), active: format_num(active),
created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(), wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
nsfw: res["data"]["over18"].as_bool().unwrap_or_default(), nsfw: res["data"]["over18"].as_bool().unwrap_or_default(),
}) })
@ -494,6 +531,11 @@ pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
link: Some(utils::get_post_url(&post)), link: Some(utils::get_post_url(&post)),
author: Some(post.author.name), author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)), content: Some(rewrite_urls(&post.body)),
description: Some(format!(
"<a href='{}{}'>Comments</a>",
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
post.permalink
)),
..Default::default() ..Default::default()
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),

View File

@ -1,11 +1,14 @@
#![allow(clippy::cmp_owned)]
// CRATES // CRATES
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 crate::{config, utils};
use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template;
use time::{macros::format_description, OffsetDateTime}; use time::{macros::format_description, OffsetDateTime};
use log::trace;
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
@ -109,6 +112,7 @@ async fn user(name: &str) -> Result<User, String> {
// Send a request to the url // Send a request to the url
json(path, false).await.map(|res| { json(path, false).await.map(|res| {
trace!("User info from r/{} : {}",name,res["data"]);
// Grab creation date as unix timestamp // Grab creation date as unix timestamp
let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH); let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH);

View File

@ -1,17 +1,21 @@
#![allow(dead_code)] #![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use crate::config::{self, get_setting}; use crate::config::{self, get_setting};
// //
// CRATES // CRATES
// //
use crate::{client::json, server::RequestExt}; use crate::{client::json, server::RequestExt};
use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use log::error; use log::error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rinja::Template;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
@ -45,6 +49,7 @@ pub enum ResourceType {
} }
// Post flair with content, background color and foreground color // Post flair with content, background color and foreground color
#[derive(Serialize)]
pub struct Flair { pub struct Flair {
pub flair_parts: Vec<FlairPart>, pub flair_parts: Vec<FlairPart>,
pub text: String, pub text: String,
@ -53,7 +58,7 @@ pub struct Flair {
} }
// Part of flair, either emoji or text // Part of flair, either emoji or text
#[derive(Clone)] #[derive(Clone, Serialize)]
pub struct FlairPart { pub struct FlairPart {
pub flair_part_type: String, pub flair_part_type: String,
pub value: String, pub value: String,
@ -95,12 +100,14 @@ impl FlairPart {
} }
} }
#[derive(Serialize)]
pub struct Author { pub struct Author {
pub name: String, pub name: String,
pub flair: Flair, pub flair: Flair,
pub distinguished: String, pub distinguished: String,
} }
#[derive(Serialize)]
pub struct Poll { pub struct Poll {
pub poll_options: Vec<PollOption>, pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String), pub voting_end_timestamp: (String, String),
@ -128,6 +135,7 @@ impl Poll {
} }
} }
#[derive(Serialize)]
pub struct PollOption { pub struct PollOption {
pub id: u64, pub id: u64,
pub text: String, pub text: String,
@ -157,13 +165,14 @@ impl PollOption {
} }
// Post flags with nsfw and stickied // Post flags with nsfw and stickied
#[derive(Serialize)]
pub struct Flags { pub struct Flags {
pub spoiler: bool, pub spoiler: bool,
pub nsfw: bool, pub nsfw: bool,
pub stickied: bool, pub stickied: bool,
} }
#[derive(Debug)] #[derive(Debug, Serialize)]
pub struct Media { pub struct Media {
pub url: String, pub url: String,
pub alt_url: String, pub alt_url: String,
@ -263,6 +272,7 @@ impl Media {
} }
} }
#[derive(Serialize)]
pub struct GalleryMedia { pub struct GalleryMedia {
pub url: String, pub url: String,
pub width: i64, pub width: i64,
@ -303,6 +313,7 @@ impl GalleryMedia {
} }
// Post containing content, metadata and media // Post containing content, metadata and media
#[derive(Serialize)]
pub struct Post { pub struct Post {
pub id: String, pub id: String,
pub title: String, pub title: String,
@ -469,7 +480,7 @@ pub struct Comment {
pub prefs: Preferences, pub prefs: Preferences,
} }
#[derive(Default, Clone)] #[derive(Default, Clone, Serialize)]
pub struct Award { pub struct Award {
pub name: String, pub name: String,
pub icon_url: String, pub icon_url: String,
@ -483,6 +494,7 @@ impl std::fmt::Display for Award {
} }
} }
#[derive(Serialize)]
pub struct Awards(pub Vec<Award>); pub struct Awards(pub Vec<Award>);
impl std::ops::Deref for Awards { impl std::ops::Deref for Awards {
@ -495,7 +507,7 @@ impl std::ops::Deref for Awards {
impl std::fmt::Display for Awards { impl std::fmt::Display for Awards {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.iter().fold(Ok(()), |result, award| result.and_then(|()| writeln!(f, "{award}"))) self.iter().try_fold((), |_, award| writeln!(f, "{award}"))
} }
} }
@ -573,8 +585,10 @@ pub struct Subreddit {
pub info: String, pub info: String,
// pub moderators: Vec<String>, // pub moderators: Vec<String>,
pub icon: String, pub icon: String,
pub banner: String,
pub members: (String, String), pub members: (String, String),
pub active: (String, String), pub active: (String, String),
pub created: String,
pub wiki: bool, pub wiki: bool,
pub nsfw: bool, pub nsfw: bool,
} }
@ -595,6 +609,7 @@ pub struct Preferences {
pub available_mascots: Vec<String>, pub available_mascots: Vec<String>,
pub theme: String, pub theme: String,
pub mascot: String, pub mascot: String,
pub redsunlib_colorway: String,
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
@ -602,7 +617,9 @@ pub struct Preferences {
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,
pub video_quality: String,
pub hide_sidebar_and_summary: String, pub hide_sidebar_and_summary: String,
pub hide_banner: String,
pub use_hls: String, pub use_hls: String,
pub ffmpeg_video_downloads: String, pub ffmpeg_video_downloads: String,
pub autoplay_videos: String, pub autoplay_videos: String,
@ -611,6 +628,7 @@ pub struct Preferences {
pub comment_sort: String, pub comment_sort: String,
pub post_sort: String, pub post_sort: String,
pub subscriptions: Vec<String>, pub subscriptions: Vec<String>,
pub quicklist: Vec<String>,
pub filters: Vec<String>, pub filters: Vec<String>,
pub hide_awards: String, pub hide_awards: String,
pub hide_score: String, pub hide_score: String,
@ -648,16 +666,19 @@ impl Preferences {
available_mascots: mascots, available_mascots: mascots,
theme: setting(req, "theme"), theme: setting(req, "theme"),
mascot: setting(req, "mascot"), mascot: setting(req, "mascot"),
redsunlib_colorway: setting(req, "redsunlib_colorway"),
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"), 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"),
hide_banner: setting(req, "hide_banner"),
blur_nsfw: setting(req, "blur_nsfw"), blur_nsfw: setting(req, "blur_nsfw"),
use_hls: setting(req, "use_hls"), use_hls: setting(req, "use_hls"),
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"), ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
hide_hls_notification: setting(req, "hide_hls_notification"), hide_hls_notification: setting(req, "hide_hls_notification"),
video_quality: setting(req, "video_quality"),
autoplay_videos: setting(req, "autoplay_videos"), autoplay_videos: setting(req, "autoplay_videos"),
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()), fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"), disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
@ -665,6 +686,7 @@ impl Preferences {
post_sort: setting(req, "post_sort"), post_sort: setting(req, "post_sort"),
subscriptions: setting(req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), subscriptions: setting(req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
filters: setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), filters: setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
quicklist: setting(req, "quicklist").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
hide_awards: setting(req, "hide_awards"), hide_awards: setting(req, "hide_awards"),
hide_score: setting(req, "hide_score"), hide_score: setting(req, "hide_score"),
} }
@ -937,12 +959,19 @@ pub fn rewrite_urls(input_text: &str) -> String {
// Rewrite Reddit links to Redlib // Rewrite Reddit links to Redlib
REDDIT_REGEX.replace_all(input_text, r#"href="/"#) REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
.to_string(); .to_string();
text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default())) loop {
.to_string() if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
// Remove (html-encoded) "\" from URLs. break;
.replace("%5C", "") } else {
.replace("\\_", "_"); text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
}
}
// Remove (html-encoded) "\" from URLs.
text1 = text1.replace("%5C", "").replace("\\_", "_");
// Rewrite external media previews to Redlib // Rewrite external media previews to Redlib
loop { loop {
@ -998,6 +1027,83 @@ pub fn rewrite_urls(input_text: &str) -> String {
} }
} }
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
/* Create the paths we'll use to look for our data inside the json.
Because we don't know the name of any given emote we use a wildcard to parse them. */
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
// Extract all of the results from those json paths
let link_nodes = media_metadata.json_path(&link_path);
let id_nodes = media_metadata.json_path(&id_path);
// Initialize our vectors
let mut id_vec = Vec::new();
let mut link_vec = Vec::new();
// Add the relevant data to each of our vectors so we can access it by number later
for current_id in id_nodes {
id_vec.push(current_id)
}
for current_link in link_nodes {
link_vec.push(current_link)
}
/* Set index to the length of link_vec.
This is one larger than we'll actually be looking at, but we correct that later */
let mut index = link_vec.len();
// Comment needs to be in scope for when we call rewrite_urls()
let mut comment = comment;
/* Loop until index hits zero.
This also prevents us from trying to do anything on an empty vector */
while index != 0 {
/* Subtract 1 from index to get the real index we should be looking at.
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
index -= 1;
// Convert our current index in id_vec into a string so we can search through it with regex
let current_id = id_vec[index].to_string();
/* The ID number can be multiple lengths, so we capture it with regex.
We also want to only attempt anything when we get matches to avoid panicking */
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(&current_id) {
// Format the ID to include the colons it has in the comment text
let id = format!(":{}:", &id_capture[1]);
// Convert current link to string to search through it with the regex
let link = link_vec[index].to_string();
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
It's a good idea and makes everything look nicer, so we'll do the same. */
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
let to_replace_with = format!(
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
&link_capture[1]
);
// Inside the comment replace the ID we found with the string that will embed the image
comment = comment.replace(&id, &to_replace_with).to_string();
}
}
}
// Call rewrite_urls() to transform any other Reddit links
rewrite_urls(&comment)
}
// Format vote count to a string that will be displayed. // Format vote count to a string that will be displayed.
// Append `m` and `k` for millions and thousands respectively, and // Append `m` and `k` for millions and thousands respectively, and
// round to the nearest tenth. // round to the nearest tenth.
@ -1319,3 +1425,11 @@ fn test_url_path_basename() {
// empty path // empty path
assert_eq!(url_path_basename("/"), ""); assert_eq!(url_path_basename("/"), "");
} }
#[test]
fn test_rewriting_emotes() {
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
}

55
static/check_update.js Normal file
View File

@ -0,0 +1,55 @@
async function checkInstanceUpdateStatus() {
try {
const response = await fetch('/commits.json');
const text = await response.text();
const entries = JSON.parse(text);
const localCommit = document.getElementById('git_commit').dataset.value;
let statusMessage = '';
if (entries.length > 0) {
const commitHashes = Array.from(entries).map(entry => {
return entry.sha
});
const commitIndex = commitHashes.indexOf(localCommit);
if (commitIndex === 0) {
statusMessage = '✅ Instance is up to date.';
} else if (commitIndex > 0) {
statusMessage = `⚠️ This instance is not up to date and is ${commitIndex} commits old. Test and confirm on an up-to-date instance before reporting.`;
document.getElementById('error-318').remove();
} else {
statusMessage = `⚠️ This instance is not up to date and is at least ${commitHashes.length} commits old. Test and confirm on an up-to-date instance before reporting.`;
document.getElementById('error-318').remove();
}
} else {
statusMessage = '⚠️ Unable to fetch commit information.';
}
document.getElementById('update-status').innerText = statusMessage;
} catch (error) {
console.error('Error fetching commits:', error);
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
}
}
async function checkOtherInstances() {
try {
const response = await fetch('/instances.json');
const data = await response.json();
const randomInstance = data.instances[Math.floor(Math.random() * data.instances.length)];
const instanceUrl = randomInstance.url;
// Set the href of the <a> tag to the instance URL with path included
document.getElementById('random-instance').href = instanceUrl + window.location.pathname;
//document.getElementById('random-instance').innerText = "Visit Random Instance";
} catch (error) {
console.error('Error fetching instances:', error);
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
}
}
// Set the target URL when the page loads
window.addEventListener('load', checkOtherInstances);
checkInstanceUpdateStatus();

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -41,7 +41,7 @@
:root, :root,
.dark { .dark {
/* Default & fallback theme (dark) */ /* Default & fallback theme (dark) */
--accent: aqua; --accent: #d54455;
--green: #5cff85; --green: #5cff85;
--text: white; --text: white;
--foreground: #222; --foreground: #222;
@ -62,7 +62,7 @@
/* Browser-defined light theme */ /* Browser-defined light theme */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--accent: #009a9a; --accent: #bb2b3b;
--green: #00a229; --green: #00a229;
--text: black; --text: black;
--foreground: #f5f5f5; --foreground: #f5f5f5;
@ -192,7 +192,7 @@ nav.fixed_navbar {
nav * { nav * {
color: var(--text); color: var(--text);
} }
nav #reddit, nav #red,
#code > span { #code > span {
color: var(--accent); color: var(--accent);
} }
@ -216,7 +216,9 @@ nav #links {
} }
nav #links svg { nav #links svg {
display: none; /*display: none;*/
position: relative;
/*What's nine plus ten?*/ top: /*Twenty one*/21%; /*You stupid.*/
} }
nav #redlib { nav #redlib {
@ -389,6 +391,10 @@ body > footer {
opacity: 0.8; opacity: 0.8;
} }
.footer-button {
padding-left: 10px;
}
/* / Body footer. */ /* / Body footer. */
/* Footer in content block. */ /* Footer in content block. */
@ -425,7 +431,7 @@ a:hover {
} }
svg { svg {
stroke: var(--text); color: var(--text);
} }
img[src=""] { img[src=""] {
@ -493,7 +499,7 @@ aside {
height: 100px; height: 100px;
border: 2px solid var(--accent); border: 2px solid var(--accent);
border-radius: 100%; border-radius: 100%;
padding: 10px; padding: 0px;
margin: 10px; margin: 10px;
} }
@ -519,13 +525,20 @@ aside {
margin-bottom: 20px; margin-bottom: 20px;
} }
#user_details, #user_actions,
#sub_details, #sub_actions {
#sub_actions, display: grid;
#user_actions { grid-template-columns: auto 4fr 1fr;
}
#user_details {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
#sub_details {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 279px) { @media screen and (max-width: 279px) {
#sub_actions { display: unset; } #sub_actions { display: unset; }
} }
@ -537,11 +550,9 @@ aside {
/* Subscriptions */ /* Subscriptions */
#sub_subscription,
#user_subscription, #user_subscription,
#sub_filter,
#user_filter, #user_filter,
#sub_rss, #user_quicklist,
#user_rss { #user_rss {
margin-top: 20px; margin-top: 20px;
} }
@ -550,9 +561,16 @@ aside {
margin-bottom: 20px; margin-bottom: 20px;
} }
#sub_quicklist button,
#user_quicklist button {
padding: 0px;
}
.subscribe, .subscribe,
.unsubscribe, .unsubscribe,
.filter, .filter,
.unquick,
.quick,
.unfilter { .unfilter {
padding: 10px 20px; padding: 10px 20px;
border-radius: 5px; border-radius: 5px;
@ -564,12 +582,22 @@ aside {
color: var(--foreground); color: var(--foreground);
background-color: var(--accent); background-color: var(--accent);
} }
.quick {
color: var(--foreground);
background-color: var(--accent);
height: 18px;
}
.unsubscribe, .unsubscribe,
.unfilter { .unfilter {
color: var(--text); color: var(--text);
background-color: var(--highlighted); background-color: var(--highlighted);
} }
.unquick {
color: var(--text);
background-color: var(--highlighted);
height: 18px;
}
/* Feeds */ /* Feeds */
@ -616,6 +644,7 @@ aside {
#feed_list > .selected { #feed_list > .selected {
background-color: var(--accent); background-color: var(--accent);
color: var(--foreground); color: var(--foreground);
font-weight: 500;
} }
#feed_list > a:not(.selected):hover { #feed_list > a:not(.selected):hover {
@ -870,6 +899,7 @@ main > * > footer > a {
#listing_options > a.selected { #listing_options > a.selected {
background: var(--accent); background: var(--accent);
color: var(--foreground); color: var(--foreground);
font-weight: 500;
} }
#sort_options > a:not(.selected):hover, #sort_options > a:not(.selected):hover,
@ -1242,6 +1272,10 @@ a.search_subreddit:hover {
width: 100%; width: 100%;
} }
.highlighted .post_poll {
padding: 15px 0 5px;
}
/* Used only for text post preview */ /* Used only for text post preview */
.post_preview { .post_preview {
-webkit-mask-image: linear-gradient(180deg, #000 60%, transparent); -webkit-mask-image: linear-gradient(180deg, #000 60%, transparent);
@ -1446,6 +1480,7 @@ a.search_subreddit:hover {
.comment:not([id]) .comment_data > .comment_link { .comment:not([id]) .comment_data > .comment_link {
display: -webkit-box; display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-break: break-all; word-break: break-all;
@ -1786,16 +1821,28 @@ input[type="submit"] {
-moz-appearance: none; -moz-appearance: none;
} }
#settings_subs .unsubscribe { #settings_subs form {
margin-left: 30px; margin-left: auto;
}
#settings_subs div {
display: flex;
height: 45px;
align-items: center;
} }
#settings_subs a { #settings_subs a {
color: var(--accent); color: var(--accent);
} }
#settings_filters .unsubscribe { #settings_filters form {
margin-left: 30px; margin-left: auto;
}
#settings_filters div {
display: flex;
height: 45px;
align-items: center;
} }
#settings_filters a { #settings_filters a {
@ -1818,7 +1865,7 @@ input[type="submit"] {
width: 100%; width: 100%;
} }
.md > p:not(:first-child):not(:last-child) { .md > p:not(:first-child) {
margin-top: 20px; margin-top: 20px;
} }
@ -1943,6 +1990,13 @@ th {
color: var(--accent); color: var(--accent);
} }
#issue_warning {
color: var(--popup-toreddit-text);
background: var(--popup-background);
border-radius: 5px;
padding: 10px;
}
/* Messages */ /* Messages */
#duplicates_msg h3 { #duplicates_msg h3 {
@ -2239,8 +2293,10 @@ th {
max-width: 20em; max-width: 20em;
} }
.download { .download {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
padding-top: 3px;
padding-bottom: 1px;
font-size: 20px; font-size: 20px;
font-weight: 900; font-weight: 900;
color: var(--accent); color: var(--accent);
@ -2254,4 +2310,11 @@ th {
.download:active { .download:active {
background-color: var(--background); background-color: var(--background);
}
.rotate{
animation: spinner 1.5s linear infinite;
padding: 0px;
}
@keyframes spinner {
to { transform: rotate(360deg); }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,9 @@
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 // @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
let ffmpeg = null; let ffmpeg = null;
let loadingsvg = `<svg class="rotate" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 4.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 0 0 0-15M1.5 12C1.5 6.201 6.201 1.5 12 1.5S22.5 6.201 22.5 12S17.799 22.5 12 22.5S1.5 17.799 1.5 12" opacity="0.1"/><path fill="currentColor" d="M12 4.5a7.46 7.46 0 0 0-5.187 2.083a1.5 1.5 0 0 1-2.075-2.166A10.46 10.46 0 0 1 12 1.5a1.5 1.5 0 0 1 0 3"/></g></svg>`;
let downloadsvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M20 15a1 1 0 0 1 1 1v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4a1 1 0 1 1 2 0v4h14v-4a1 1 0 0 1 1-1M12 2a1 1 0 0 1 1 1v10.243l2.536-2.536a1 1 0 1 1 1.414 1.414l-4.066 4.066a1.25 1.25 0 0 1-1.768 0L7.05 12.121a1 1 0 1 1 1.414-1.414L11 13.243V3a1 1 0 0 1 1-1"/></g></svg>`;
(function () { (function () {
if (Hls.isSupported()) { if (Hls.isSupported()) {
@ -13,7 +17,7 @@ let ffmpeg = null;
var autoplay = oldVideo.classList.contains("hls_autoplay"); var autoplay = oldVideo.classList.contains("hls_autoplay");
// If HLS is supported natively then don't use hls.js // If HLS is supported natively then don't use hls.js
if (oldVideo.canPlayType(source.type) === "probably") { if (oldVideo.canPlayType(source.type) === "probably" && !downloadsEnabled) {
if (autoplay) { if (autoplay) {
oldVideo.play(); oldVideo.play();
} }
@ -82,7 +86,7 @@ let ffmpeg = null;
var mediaStream = []; var mediaStream = [];
var downloadButton = document.createElement('button'); var downloadButton = document.createElement('button');
downloadButton.classList.add('video-options','download'); downloadButton.classList.add('video-options','download');
downloadButton.innerText = "⏳" downloadButton.innerHTML = loadingsvg
const mergeStreams = async () => { const mergeStreams = async () => {
if (ffmpeg === null) { if (ffmpeg === null) {
ffmpeg = new FFmpeg(); ffmpeg = new FFmpeg();
@ -115,7 +119,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, {type: 'video/mp4'}); saveAs(new Blob([data.buffer], {type: 'video/mp4'}),filename);
return return
} }
function saveAs(blob, filename) { // Yeah ok... function saveAs(blob, filename) { // Yeah ok...
@ -157,11 +161,11 @@ let ffmpeg = null;
var isDownloading = false var isDownloading = false
function startDownload() { function startDownload() {
if (!isDownloading) { isDownloading = true } else { return } if (!isDownloading) { isDownloading = true } else { return }
downloadButton.innerText = "⏳" downloadButton.innerHTML = loadingsvg
mergeStreams() mergeStreams()
.then(_ => { .then(_ => {
isDownloading = false isDownloading = false
downloadButton.innerText = "⭳" downloadButton.innerHTML = downloadsvg
}); });
} }
@ -178,7 +182,7 @@ let ffmpeg = null;
waitForLoad(_ => flag === true) waitForLoad(_ => flag === true)
.then(_ => { .then(_ => {
downloadButton.innerText = "⭳" downloadButton.innerHTML = downloadsvg
downloadButton.addEventListener('click', startDownload); downloadButton.addEventListener('click', startDownload);
}); });
} }

View File

@ -27,6 +27,8 @@
<link rel="manifest" type="application/json" href="/manifest.json"> <link rel="manifest" type="application/json" href="/manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}"> <link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}">
<!-- Video quality -->
<div id="video_quality" data-value="{{ prefs.video_quality }}"></div>
{% endblock %} {% endblock %}
</head> </head>
<body class=" <body class="
@ -38,27 +40,35 @@
<nav class=" <nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}"> {% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<div id="logo"> <div id="logo">
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a> <a id="redlib" href="/">
<span id="lib" {% if prefs.redsunlib_colorway == "on" %}style="color: #ff8585;"{% endif %}">red</span><span id="reddit" {% if prefs.redsunlib_colorway == "on" %}style="color: #ffbfbf;"{% endif %}>sun</span><span id="lib" {% if prefs.redsunlib_colorway == "on" %}style="color: #ff8585;"{% endif %}>lib.</span>
</a>
{% block subscriptions %}{% endblock %} {% block subscriptions %}{% endblock %}
</div> </div>
{% block search %}{% endblock %} {% block search %}{% endblock %}
<div id="links"> <div id="links">
<a id="reddit_link" {% if prefs.disable_visit_reddit_confirmation != "on" %}href="#popup"{% else %}href="https://www.reddit.com{{ url }}" rel="nofollow"{% endif %}> <a id="reddit_link" {% if prefs.disable_visit_reddit_confirmation != "on" %}href="#popup"{% else %}href="https://www.reddit.com{{ url }}" rel="nofollow"{% endif %}>
<span>reddit</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <title>redirect to reddit</title>
<path d="M22 2L12 22"/> <g fill="none">
<path d="M2 6.70587C3.33333 8.07884 3.33333 11.5971 3.33333 11.5971M3.33333 19.647V11.5971M3.33333 11.5971C3.33333 11.5971 5.125 7.47817 8 7.47817C10.875 7.47817 12 8.85114 12 8.85114"/> <path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M6.301 6a4 4 0 0 1 3.312 1.756l.118.186l4.253 7.087a2 2 0 0 0 1.553.965L15.7 16h1.194l.02-.415l.022-.36l.012-.159c.027-.346.352-.557.631-.41l.306.164l.36.203l.198.117l.43.263l.229.147l.463.31l.21.147l.377.273l.315.24l.133.104c.236.188.225.566-.023.762l-.28.217l-.34.252l-.4.282l-.456.305l-.462.291l-.416.249l-.365.205l-.307.165c-.275.143-.572-.036-.598-.36l-.025-.347l-.024-.415l-.01-.23H15.7a4 4 0 0 1-3.312-1.756l-.118-.186l-4.253-7.087a2 2 0 0 0-1.553-.965L6.3 8H4a1 1 0 0 1-.117-1.993L4 6zm3.714 7.643a1 1 0 0 1 .342 1.371l-.626 1.044A4 4 0 0 1 6.301 18H4a1 1 0 1 1 0-2h2.301a2 2 0 0 0 1.715-.971l.627-1.043a1 1 0 0 1 1.371-.344Zm7.563-8.988l.306.165l.36.203l.198.117l.43.263l.229.147l.463.31l.21.147l.377.273l.315.24l.133.104c.236.188.225.566-.023.762l-.28.217l-.34.252q-.186.135-.4.282l-.456.305l-.462.291l-.416.249l-.365.206l-.307.164c-.275.143-.572-.036-.598-.36l-.025-.347l-.024-.415l-.01-.23H15.7a2 2 0 0 0-1.627.836l-.088.135l-.626 1.043a1 1 0 0 1-1.77-.925l.055-.104l.626-1.043a4 4 0 0 1 3.209-1.936l.22-.006h1.195l.02-.415l.022-.36l.012-.159c.027-.346.352-.557.631-.41Z" />
</g>
</svg> </svg>
<span>reddit</span>
</a> </a>
{% if prefs.disable_visit_reddit_confirmation != "on" %} {% if prefs.disable_visit_reddit_confirmation != "on" %}
{% call utils::visit_reddit_confirmation(url) %} {% call utils::visit_reddit_confirmation(url) %}
{% endif %} {% endif %}
<a id="settings_link" href="/settings"> <a id="settings_link" href="/settings">
<span>settings</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <g fill="none" fill-rule="evenodd">
<title>settings</title> <title>settings</title>
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/> <path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M14.035 2.809c.37-.266.89-.39 1.401-.203a10 10 0 0 1 2.982 1.725c.417.35.57.861.524 1.313c-.075.753.057 1.48.42 2.106c.32.557.802.997 1.39 1.307l.225.11c.414.187.782.576.875 1.113a10 10 0 0 1 0 3.44c-.083.484-.39.847-.753 1.051l-.122.063c-.69.31-1.254.79-1.616 1.416c-.362.627-.494 1.353-.419 2.106c.045.452-.107.964-.524 1.313a10 10 0 0 1-2.982 1.725a1.51 1.51 0 0 1-1.4-.203C13.42 20.75 12.723 20.5 12 20.5s-1.42.249-2.035.691a1.51 1.51 0 0 1-1.401.203a10 10 0 0 1-2.982-1.725a1.51 1.51 0 0 1-.524-1.313c.075-.753-.058-1.48-.42-2.106a3.4 3.4 0 0 0-1.39-1.307l-.225-.11a1.51 1.51 0 0 1-.875-1.113a10 10 0 0 1 0-3.44c.083-.484.39-.847.753-1.051l.122-.062c.69-.311 1.254-.79 1.616-1.417c.361-.626.494-1.353.419-2.106a1.51 1.51 0 0 1 .524-1.313a10 10 0 0 1 2.982-1.725a1.51 1.51 0 0 1 1.4.203c.615.442 1.312.691 2.036.691s1.42-.249 2.035-.691m.957 1.769c-.866.57-1.887.922-2.992.922s-2.126-.353-2.992-.922A8 8 0 0 0 7.068 5.7c.06 1.033-.145 2.093-.697 3.05c-.553.956-1.368 1.663-2.293 2.128a8 8 0 0 0 0 2.242c.925.465 1.74 1.172 2.293 2.13c.552.955.757 2.015.697 3.048a8 8 0 0 0 1.94 1.123c.866-.57 1.887-.922 2.992-.922s2.126.353 2.992.922a8 8 0 0 0 1.94-1.122c-.06-1.034.145-2.094.697-3.05c.552-.957 1.368-1.664 2.293-2.13a8 8 0 0 0 0-2.24c-.925-.466-1.74-1.173-2.293-2.13c-.552-.956-.757-2.016-.697-3.05a8 8 0 0 0-1.94-1.122ZM12 8a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4" />
</g>
</svg> </svg>
<span>settings</span>
</a> </a>
</div> </div>
</nav> </nav>

View File

@ -6,10 +6,19 @@
<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 <h3 id="update-status"></h3>
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 <br />
an issue</a></h3> <h3 id="update-status"></h3>
<br>
<div id="git_commit" data-value="{{ crate::instance_info::INSTANCE_INFO.git_commit }}"></div>
<script src="/check_update.js"></script>
<h3>Expected something to work? Try a random <a id="random-instance">upstream instance.</a></h3>
<br />
<h3 id="issue_warning" >!! Do <b>NOT</b> open an issue on the <a href="https://github.com/redlib-org/redlib/">redlib repository</a> with a redsunlib specific issue !!</h3>
<br />
<p id="error-318">If you're getting a "Failed to parse page JSON data" error, please check <a href="https://github.com/redlib-org/redlib/issues/318" target="_blank">#318</a></p>
<br /> <br />
<h3>Head back <a href="/">home</a>?</h3> <h3>Head back <a href="/">home</a>?</h3>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -29,6 +29,11 @@
{% call utils::options(prefs.mascot, prefs.available_mascots, "system") %} {% call utils::options(prefs.mascot, prefs.available_mascots, "system") %}
</select> </select>
</div> </div>
<div class="prefs-group">
<label for="redsunlib_colorway">Force redsunlib colorway</label>
<input type="hidden" value="off" name="redsunlib_colorway">
<input type="checkbox" name="redsunlib_colorway" {% if prefs.redsunlib_colorway == "on" %}checked{% endif %}>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Interface</legend> <legend>Interface</legend>
@ -50,6 +55,26 @@
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}> <input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %} {% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
</div> </div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_banner">Hide subreddit banners</label>
<input type="hidden" value="off" name="hide_banner">
<input type="checkbox" name="hide_banner" {% if prefs.hide_banner == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Content</legend> <legend>Content</legend>
@ -82,21 +107,30 @@
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}> <input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
</div> </div>
{% endif %} {% endif %}
<div class="prefs-group">
<label for="hide_awards">Hide awards</label>
<input type="hidden" value="off" name="hide_awards">
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_score">Hide score</label>
<input type="hidden" value="off" name="hide_score">
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
</div>
</fieldset>
<fieldset>
<legend>Media</legend>
<div class="prefs-group">
<label for="video_quality">Video quality:</label>
<select name="video_quality" id="video_quality">
{% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %}
</select>
</div>
<div class="prefs-group"> <div class="prefs-group">
<label for="autoplay_videos">Autoplay videos</label> <label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos"> <input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}> <input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group"> <div class="prefs-group">
<label for="use_hls">Use HLS for videos</label> <label for="use_hls">Use HLS for videos</label>
{% if prefs.ffmpeg_video_downloads != "on" %} {% if prefs.ffmpeg_video_downloads != "on" %}
@ -123,28 +157,13 @@
<input type="hidden" value="off" name="hide_hls_notification"> <input type="hidden" value="off" name="hide_hls_notification">
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}> <input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group">
<label for="hide_awards">Hide awards</label>
<input type="hidden" value="off" name="hide_awards">
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_score">Hide score</label>
<input type="hidden" value="off" name="hide_score">
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset> </fieldset>
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>
</form> </form>
<div id="settings_note"> <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><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> <p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&redsunlib_colorway={{ prefs.redsunlib_colorway }}&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}}&hide_banner={{ prefs.hide_banner}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}&quicklist={{ prefs.quicklist.join("%2B") }}">this link</a>.</p>
</div> </div>
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs"> <div class="prefs" id="settings_subs">

View File

@ -100,17 +100,40 @@
<a href="/r/{{ sub.name }}/wiki/index">Wiki</a> <a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
</div> </div>
{% endif %} {% endif %}
<div id="sub_meta"> {% if prefs.hide_banner != "on" %}
{% block head %}
{% call super() %}
<link rel="preload" as="image" href="{{ sub.banner }}">
{% endblock %}
{% endif %}
<div {% if prefs.hide_banner != "on" %}style="background: linear-gradient(to bottom, rgba(255,255,255,0) 10%, var(--outside)), url({{ sub.banner }});background-size: 100%;background-size: cover;background-position: center center;"{% endif %} id="iconbanner">
<img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}"> <img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}">
</div>
<div id="sub_meta">
<h1 id="sub_title">{{ sub.title }}</h1> <h1 id="sub_title">{{ sub.title }}</h1>
<p id="sub_name">r/{{ sub.name }}</p> <p id="sub_name">r/{{ sub.name }}</p>
{% if crate::utils::enable_rss() %}
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M18 3a3 3 0 0 1 2.995 2.824L21 6v12a3 3 0 0 1-2.824 2.995L18 21H6a3 3 0 0 1-2.995-2.824L3 18V6a3 3 0 0 1 2.824-2.995L6 3zM8.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M8 10.5a1 1 0 1 0 0 2a3.5 3.5 0 0 1 3.5 3.5a1 1 0 1 0 2 0A5.5 5.5 0 0 0 8 10.5M8.5 7q-.285 0-.566.019a1 1 0 0 0 .132 1.995a6.5 6.5 0 0 1 6.92 6.92a1 1 0 1 0 1.995.132A8.5 8.5 0 0 0 8.5 7" />
</g>
</svg>
</button >
</a>
{% endif %}
<p id="sub_description">{{ sub.description }}</p> <p id="sub_description">{{ sub.description }}</p>
<div id="sub_details"> <div id="sub_details">
<label>Members</label> <label>Members</label>
<label>Active</label> <label>Active</label>
<label>Created</label>
<div title="{{ sub.members.1 }}">{{ sub.members.0 }}</div> <div title="{{ sub.members.1 }}">{{ sub.members.0 }}</div>
<div title="{{ sub.active.1 }}">{{ sub.active.0 }}</div> <div title="{{ sub.active.1 }}">{{ sub.active.0 }}</div>
<div>{{ sub.created }}</div>
</div> </div>
<hr>
<div id="sub_actions"> <div id="sub_actions">
<div id="sub_subscription"> <div id="sub_subscription">
{% if prefs.subscriptions.contains(sub.name) %} {% if prefs.subscriptions.contains(sub.name) %}
@ -134,13 +157,31 @@
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if crate::utils::enable_rss() %} <div id="sub_quicklist">
<div id="sub_rss"> {% if prefs.quicklist.contains(sub.name) %}
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}"> <form action="/r/{{ sub.name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
<button class="subscribe">RSS feed</button > <button>
</a> <svg class="unquick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M16 2a3 3 0 0 1 3 3v11h-3a3 3 0 0 0-2.997 2.87L12 18.202l-4.668 3.112C6.335 21.978 5 21.264 5 20.066V5a3 3 0 0 1 3-3zm6 16a1 1 0 0 1 .117 1.993L22 20h-6a1 1 0 0 1-.117-1.993L16 18z" />
</g>
</svg>
</button>
</form>
{% else %}
<form action="/r/{{ sub.name }}/quicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="quick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M10 2a3 3 0 0 0-3 3a3 3 0 0 0-3 3v13.018c0 1.226 1.39 1.934 2.382 1.213l4.118-2.995l4.118 2.995c.991.721 2.382.013 2.382-1.213v-2.236l.618.45c.991.72 2.382.012 2.382-1.214V5a3 3 0 0 0-3-3zm7 14.309l1 .727V5a1 1 0 0 0-1-1h-7a1 1 0 0 0-1 1h5a3 3 0 0 1 3 3z" />
</g>
</svg>
</button>
</form>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</details> </details>
<details class="panel" id="sidebar" open> <details class="panel" id="sidebar" open>

View File

@ -61,7 +61,7 @@ body %}
<summary class="comment_data"> <summary class="comment_data">
<a <a
class="comment_link" class="comment_link"
href="{{ post.permalink }}" href="{{ post.permalink }}#{{ post.id }}"
title="{{ post.link_title }}" title="{{ post.link_title }}"
>{{ post.link_title }}</a >{{ post.link_title }}</a
> >
@ -123,6 +123,18 @@ body %}
/> />
<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>
{% if crate::utils::enable_rss() %}
<a href="/r/{{ user.name }}.rss" title="RSS feed for r/{{ user.name }}">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M18 3a3 3 0 0 1 2.995 2.824L21 6v12a3 3 0 0 1-2.824 2.995L18 21H6a3 3 0 0 1-2.995-2.824L3 18V6a3 3 0 0 1 2.824-2.995L6 3zM8.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M8 10.5a1 1 0 1 0 0 2a3.5 3.5 0 0 1 3.5 3.5a1 1 0 1 0 2 0A5.5 5.5 0 0 0 8 10.5M8.5 7q-.285 0-.566.019a1 1 0 0 0 .132 1.995a6.5 6.5 0 0 1 6.92 6.92a1 1 0 1 0 1.995.132A8.5 8.5 0 0 0 8.5 7" />
</g>
</svg>
</button >
</a>
{% endif %}
<div id="user_description">{{ user.description }}</div> <div id="user_description">{{ user.description }}</div>
<div id="user_details"> <div id="user_details">
<label>Karma</label> <label>Karma</label>
@ -166,16 +178,31 @@ body %}
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if crate::utils::enable_rss() %} <div id="user_quicklist">
<div id="user_rss"> {% if prefs.quicklist.contains(name) %}
<a <form action="/r/{{ name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
href="/u/{{ user.name }}.rss" <button>
title="RSS feed for u/{{ user.name }}" <svg class="unquick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
> <g fill="none">
<button class="subscribe">RSS feed</button> <path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
</a> <path fill="currentColor" d="M16 2a3 3 0 0 1 3 3v11h-3a3 3 0 0 0-2.997 2.87L12 18.202l-4.668 3.112C6.335 21.978 5 21.264 5 20.066V5a3 3 0 0 1 3-3zm6 16a1 1 0 0 1 .117 1.993L22 20h-6a1 1 0 0 1-.117-1.993L16 18z" />
</g>
</svg>
</button>
</form>
{% else %}
<form action="/r/{{ name }}/quicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="quick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M10 2a3 3 0 0 0-3 3a3 3 0 0 0-3 3v13.018c0 1.226 1.39 1.934 2.382 1.213l4.118-2.995l4.118 2.995c.991.721 2.382.013 2.382-1.213v-2.236l.618.45c.991.72 2.382.012 2.382-1.214V5a3 3 0 0 0-3-3zm7 14.309l1 .727V5a1 1 0 0 0-1-1h-7a1 1 0 0 0-1 1h5a3 3 0 0 1 3 3z" />
</g>
</svg>
</button>
</form>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</aside> </aside>

View File

@ -41,6 +41,12 @@
<details id="feeds"> <details id="feeds">
<summary>Feeds</summary> <summary>Feeds</summary>
<div id="feed_list"> <div id="feed_list">
{% if prefs.quicklist.len() > 0 %}
<p>QUICK ACCESS FEEDS</p>
{% for sub in prefs.quicklist %}
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{% if sub.starts_with("u_") -%}{%let sub = format!("u/{}", &sub[2..]) -%}{{ sub }}{% else -%}{{ sub }}{% endif -%}</a>
{% endfor %}
{% endif %}
<p>MAIN FEEDS</p> <p>MAIN FEEDS</p>
<a href="/">Home</a> <a href="/">Home</a>
<a href="/r/popular">Popular</a> <a href="/r/popular">Popular</a>
@ -48,7 +54,7 @@
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<p>REDDIT FEEDS</p> <p>REDDIT FEEDS</p>
{% for sub in prefs.subscriptions %} {% for sub in prefs.subscriptions %}
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a> <a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{% if sub.starts_with("u_") -%}{%let sub = format!("u/{}", &sub[2..]) -%}{{ sub }}{% else -%}{{ sub }}{% endif -%}</a>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@ -157,7 +163,10 @@
{% endif %} {% endif %}
<!-- POST BODY --> <!-- POST BODY -->
<div class="post_body">{{ post.body|safe }}</div> <div class="post_body">
{{ post.body|safe }}
{% call poll(post) %}
</div>
<div class="post_score" title="{{ post.score.1 }}"> <div class="post_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %} {% if prefs.hide_score != "on" %}
{{ post.score.0 }} {{ post.score.0 }}