Compare commits

..

192 Commits

Author SHA1 Message Date
1644e68e43 v0.35.1 2024-06-28 13:34:12 +12:00
f5d4cc49fb fix some default options #7 2024-06-28 13:33:14 +12:00
f22f7841a8 Merge remote-tracking branch 'upstream/main' 2024-06-28 12:40:15 +12:00
b6c6e64cfe move the restore note
my scroll wheel will fall off if i have to scroll to get down there ohmagaw
2024-06-28 12:31:31 +12:00
6d3db31b11 Merge pull request 'README updates' (#9) from README-update into main
Reviewed-on: #9
2024-06-28 00:34:03 +12:00
f0eb496d6a fix README.md 2024-06-28 00:32:20 +12:00
e984646ae6 Update README.md
add information about the name, a thank you and bits here and there
2024-06-28 00:07:29 +12:00
60c0d63583 nsfw_landing style & function changes
theme the NSFW title and allow the user to directly change their setting
2024-06-27 17:43:43 +12:00
3bd8b511a7 fix(oauth): strengthen sync guarantees 2024-06-26 23:41:26 -04:00
8c5aaaa33d feat(scripts): add load testing 2024-06-26 23:40:31 -04:00
bd0a312487 add disclaimer 2024-06-27 15:04:58 +12:00
023cc8505b Merge pull request #158 from redlib-org/oauth_proper_atomics
fix(oauth): reset rate limit earlier in refresh cycle
2024-06-26 22:20:00 -04:00
2e476dea63 fix(oauth): reset rate limit earlier in refresh cycle 2024-06-26 22:16:41 -04:00
f398d16c22 fix ffmpeg_video_downloads on cookie restore 2024-06-27 14:13:16 +12:00
d045a5760a Merge pull request #156 from redlib-org/fix_oauth_ratelimit
feat(oauth): roll over oauth key on rate limit
2024-06-26 19:33:04 -04:00
07bf20dbc0 feat(oauth): roll over oauth key on rate limit 2024-06-26 19:19:30 -04:00
518bf03e04 fix(client): Add trace logging for ratelimit info, render error page if exceeded 2024-06-26 08:05:22 -04:00
00dee52320 chore(deps): Cargo update 2024-06-25 20:42:06 -04:00
48873c01b9 Merge pull request #154 from redlib-org/fix_multi_sub
fix(subreddit): handle plus-encoding errors
2024-06-25 19:52:33 -04:00
951fe400ae fix(subreddit): handle plus-encoding errors 2024-06-25 19:50:00 -04:00
bacc9e35df refactor(oauth): leave android header unmodified (fixes #131) 2024-06-25 19:28:41 -04:00
724b960112 Merge pull request #149 from pimlie/feat-blur-spoiler-previews
feat: add support to blur spoiler previews
2024-06-23 10:28:46 -04:00
ff2e274e5e v0.35.0 2024-06-23 20:48:26 +12:00
7c821d541a fix PREFS size
whoops
2024-06-23 20:44:39 +12:00
7aca5791b1 Merge branch 'hls-video-downloads' 2024-06-23 20:41:27 +12:00
8b3783096b revert ugly-layouts from hls-video-downloads
This reverts commits
97def1bab1
feda45f803
2024-06-23 20:33:32 +12:00
59b702aa15 kinda horrific download implementation 2024-06-23 19:57:23 +12:00
edf0109095 allow wasm to run 2024-06-23 19:31:30 +12:00
ecf0f91464 remove failed hls.js source map request 2024-06-23 19:30:44 +12:00
69f9d9ff3c feat: also blur spoiler previews on post view 2024-06-22 12:47:33 +02:00
1d44bd180e feat: add spoiler badge to post title 2024-06-22 12:38:13 +02:00
3301da1ef1 feat: add support to blur spoiler previews 2024-06-22 12:16:12 +02:00
138934f30c change resource links and container links 2024-06-21 23:49:37 +12:00
5396c0783e add mascots to user settings readme 2024-06-21 23:11:23 +12:00
213481ef53 Merge pull request #148 from Pix3l01/fix-healthcheck
Make server listen on both IPv6 and IPv4 by default to fix docker healthcheck
2024-06-20 08:06:27 -04:00
2d5cfcb41d Merge pull request #146 from ac615223s5/default-filter
Default filters
2024-06-20 07:58:41 -04:00
f460895cc0 chore(clippy): fix lint 2024-06-20 07:56:43 -04:00
5193164719 Merge branch 'redlib-org:main' into default-filter 2024-06-19 20:10:05 -04:00
9013e589dd chore(clippy): fix lint 2024-06-19 14:45:55 -04:00
997cd8f829 feat(bug): Improve bug reporting while keeping logs private 2024-06-19 14:45:32 -04:00
5a13b9892b chore(clippy): add lint 2024-06-19 14:28:48 -04:00
91975865b8 Make server listen on both IPv6 and IPv4 by default 2024-06-19 00:42:38 +02:00
3491e754ac Update .gitignore 2024-06-18 15:16:41 -04:00
a5a5cbb734 add defaults for mascot fixes some of #7 2024-06-18 21:00:02 +12:00
5117af4137 add hide_sidebar_and_summary to restore link 2024-06-18 20:51:02 +12:00
394e975724 Merge remote-tracking branch 'upstream/main' 2024-06-18 19:57:42 +12:00
a67cbb99a6 sync upstream 2024-06-18 19:56:04 +12:00
1408c32a4d Update .env.example 2024-06-18 00:25:29 -04:00
30944579d7 add default filter config 2024-06-18 00:21:00 -04:00
ededa849f4 Merge pull request 'mascots' (#6) from mascots into main solves #5
Reviewed-on: #6
2024-06-17 22:44:22 +12:00
1d8b6f58fb fix mascot css 2024-06-17 22:40:58 +12:00
4c6a71171b add boymoder 2024-06-17 22:29:58 +12:00
846377b586 implement mascots 2024-06-17 22:29:52 +12:00
2dbcc071a6 update CREDITS 2024-06-17 18:00:58 +12:00
a12596e1a2 remove FFmpeg from subreddit template 2024-06-16 20:02:57 +12:00
df8d36c661 serve ffmpeg.wasm 2024-06-16 19:53:16 +12:00
5018184e7c rename ffmpeg.wasm to FFmpeg 2024-06-16 18:59:47 +12:00
5c73f043cd add preference option 2024-06-16 15:01:17 +12:00
013e805ece Merge branch 'main' into ugly-layouts 2024-06-15 17:32:35 +12:00
9a7da3abce Merge pull request #142 from runofthemillgeek/fix/font-weight-in-font-face
Add font-weight range in @font-face rule
2024-06-13 23:13:27 -04:00
7fa37e48d5 Add font-weight range in @font-face rule
This makes Safari render the bold weights correctly which otherwise
looks wrong.
2024-06-11 21:16:44 +05:30
21da37246e update catppuccin
discernible difference in foreground color
2024-06-06 04:54:03 +12:00
fb0814babd change alignment fix 2024-06-06 04:43:43 +12:00
bae220d4fe fix settings selection alignment 2024-06-06 04:30:32 +12:00
97def1bab1 Disallow wide ui with old layout 2024-06-06 04:26:47 +12:00
a6f901c094 Merge pull request #128 from arulagrawal/nix-flake
add nix flake
2024-06-04 22:49:33 -04:00
afad65f204 Merge pull request #127 from pimlie/patch-1
fix: healthcheck in Dockerfile
2024-06-04 22:48:51 -04:00
c12da45059 Merge pull request #121 from bennettmsherman/patch-1
Properly apply REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY
2024-06-04 22:48:33 -04:00
10d5b4a583 fix dockerfile 2024-06-05 10:38:15 +12:00
feda45f803 "old" layout draft 2024-06-05 10:31:41 +12:00
1132d73975 Merge pull request #134 from EMarshal/main 2024-06-02 22:08:38 -04:00
8ece7562ec Remove obsolete references to latest-arm and latest-armv7 images 2024-06-02 18:45:49 -06:00
08e463fd44 Merge pull request #126 from Harm133/main
Create build-artifacts on release
2024-06-02 18:17:05 -04:00
f8f964741a super minimal rebrand 2024-06-02 22:33:45 +12:00
58c736c51b fix build not including git hash 2024-06-02 22:33:45 +12:00
336d5dcaa7 Update screenshot
yeah boymoder let your tits out
2024-06-02 01:04:14 +12:00
4c00e4ac05 Update README.md
fix claustrophobia
2024-06-02 00:37:07 +12:00
92afbe55de half rebrand the README 2024-06-02 00:34:41 +12:00
ec11a5511b add nix flake 2024-05-31 17:10:36 +04:00
9133062e68 fix healthcheck 2024-06-01 00:02:08 +12:00
Pim
3caa0592f3 fix: healthcheck in Dockerfile
Fix typo causing wget to return a warning:

```
# wget --q
wget: option '--q' is ambiguous; possibilities: '--quiet' '--quota'
```
2024-05-31 11:40:01 +02:00
190c92339e Update build-artifacts.yaml
Only build artifacts on published release
2024-05-31 10:19:44 +02:00
83d50316fb moved repo org 2024-05-31 20:00:00 +12:00
e187ccd14a Merge remote-tracking branch 'upstream/main' 2024-05-31 19:54:04 +12:00
284e03ccf9 version 2024-05-31 19:48:23 +12:00
17c7738d6e Properly apply REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY
This change fixes the REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY environment variable being ignored.
2024-05-30 20:40:16 -07:00
8a3ceaf94a Merge pull request #119 from redlib-org/fix_unauth
fix(oauth): Make Android user-agent patching unconditional
2024-05-30 18:13:43 -04:00
bd47c206a1 fix(oauth): Make Android user-agent patching unconditional 2024-05-30 18:08:45 -04:00
7a099f259f v0.34.0 2024-05-29 20:59:27 -04:00
4ea911e6b2 fix workflow again 2024-05-29 20:51:39 -04:00
31d68afdc9 fix workflow 2024-05-29 20:50:38 -04:00
96a7e155c5 v0.33.3 2024-05-29 20:49:29 -04:00
045a8852ec fix workflow 2024-05-29 20:47:44 -04:00
4cc8bf8318 v0.33.2 2024-05-29 20:05:56 -04:00
1715b36ae9 Update fix for submitted route 2024-05-29 20:02:56 -04:00
6102b08894 v0.33.1 2024-05-29 19:24:09 -04:00
892b0e89c8 Cargo fmt 2024-05-29 19:15:54 -04:00
a64e2143f3 Fix Display impl, resolving clippy 2024-05-29 18:51:20 -04:00
e13d9b7239 Merge pull request #86 from EMarshal/main
Update hide HLS env var to match documentation
2024-05-29 18:49:40 -04:00
e597ed8f06 Merge pull request #87 from ButteredCats/fix_post_footer_text
Stop post footer text from disappearing when screen is exactly 480px wide
2024-05-29 18:49:15 -04:00
8c67d33721 Merge pull request #102 from ismailkarsli/main
added geo_filter query param to /r/popular endpoint
2024-05-29 18:48:48 -04:00
26aa374bbd Merge pull request #114 from ufUNnxagpM/main
feat: add icebergDark theme
2024-05-29 18:47:19 -04:00
5b8b1e36ca Merge pull request #106 from axeII/feat/hide-summary-and-sidebar
Feature: Adds an option in the settings to hide the summary sidebar.
2024-05-29 18:46:50 -04:00
093d240530 Merge branch 'main' into feat/hide-summary-and-sidebar 2024-05-29 18:46:10 -04:00
9048565d48 Merge pull request #112 from Myzel394/fix-docs
Add `PORT` env to docs
2024-05-29 18:45:25 -04:00
6b2aab23c8 Merge pull request #92 from ButteredCats/fix_preview_captions
Update embedding Reddit preview links to include captions and support i.redd.it embeds
2024-05-29 18:44:48 -04:00
6b11d936b3 Fix clippys 2024-05-29 18:44:19 -04:00
34692359cf Merge branch 'redlib-org:main' into fix_preview_captions 2024-05-29 18:42:39 -04:00
33411a7588 Bump version 2024-05-29 18:40:09 -04:00
5a0f0f96f5 Update OAuth resources 2024-05-29 18:37:13 -04:00
273d889f1b Fix retrieval of multi-subs 2024-05-29 18:36:56 -04:00
93594fc642 feat: add icebergDark theme 2024-05-24 22:16:49 -07:00
af6f3d3b3f fix(docs): Add PORT env 2024-05-24 13:21:02 +02:00
62b791bb24 Add in support for embedding i.redd.it images and gifs and remove leftover println 2024-05-23 21:29:36 -04:00
e9af28b6eb Make sure that the extra <p></p> at the bottom of a comment containing only an image doesn't get a margin 2024-05-23 20:31:40 -04:00
2f2cded671 Make sure new system can handle both normal and external previews 2024-05-22 17:22:10 -04:00
b22fb7cd7b Fix embedded preview images having a gap from the top of a comment 2024-05-22 16:40:57 -04:00
75b0149313 Remove useless replace 2024-05-22 16:34:39 -04:00
50fad938dd Fix infinite loop when replacing text that contains dollar signs 2024-05-22 16:31:07 -04:00
b6f5831d10 feat: adds hide summary sidebar option 2024-05-13 23:49:59 +02:00
565b50646f added geo_filter query param to /r/popular endpoint 2024-04-25 14:01:06 +03:00
6484ebf897 Fix failing check 2024-04-14 17:32:10 -04:00
3f863c8991 Prevent panic if image_caption is empty, don't replace <p>'s in case text is inside them with the image, update test to reflect change in image replacing 2024-04-14 17:26:43 -04:00
e581f432dd Use substring instead of .remove and .pop, change image_text to image_caption to better reflect its usage, only replace quotes in image_caption when needed, and add comments for what some of the code does 2024-04-10 10:47:24 -04:00
2c8f5a7ac1 Make figure margins only apply to comments to bring embedded previews more in line with gallery posts 2024-04-09 18:51:32 -04:00
6d83b07aaa Update embedding Reddit preview links to include captions where applicable 2024-04-09 18:33:13 -04:00
5f67eacf93 update build 2024-04-09 22:19:40 +12:00
6684db5e82 sidebar open by default 2024-04-09 06:20:04 +12:00
1760f5a676 pagination arrows 2024-04-09 06:19:44 +12:00
8307aa1e42 show fork info 2024-04-09 06:19:00 +12:00
27f25e0fb1 Merge pull request #88 from ButteredCats/better_preview_handling
Handle preview embedding better
2024-04-08 10:04:23 -04:00
c15f25e27b Handle preview embedding better 2024-04-09 01:27:40 +12:00
20e3129d88 fix catppuccin 2024-04-09 01:13:04 +12:00
54c2ffad95 add catppuccin theme 2024-04-09 01:00:04 +12:00
6991fc6ae3 add build 2024-04-09 01:00:04 +12:00
30b27aea44 Update Dockerfile.old 2024-04-09 01:00:04 +12:00
63e2fadf4a monumental fuckup :D
revert revert
2024-04-09 01:00:04 +12:00
4ba26285d7 revert e79242c9e7
revert v0.31.2
2024-04-09 01:00:04 +12:00
89140c8cf7 Actually fix checks this time 2024-04-07 19:59:54 -04:00
99048c4683 Fix failing checks 2024-04-07 19:51:20 -04:00
ccfe7d0eeb Make embedded images keep aspect ration when shrinking 2024-04-07 19:24:55 -04:00
f7c182dcd8 Fix a couple of edge cases with image embedding and don't check if REDDIT_PREVIEW_REGEX matches before executing loop 2024-04-07 19:22:20 -04:00
9bd540d659 Stop post footer text from disappear at exactly 480px 2024-04-07 17:23:24 -04:00
4f6a14739b Update hide HLS env var to match documentation 2024-04-07 14:43:39 -06:00
7c87d63d34 Merge pull request #85 from ButteredCats/embed_images
Embed Reddit preview links
2024-04-07 14:01:47 -04:00
75b139dff2 Update image link test to account for embedded images 2024-04-07 12:08:53 -04:00
10499df423 Make image preview links embed 2024-04-07 12:07:53 -04:00
c86ca16c1a Merge pull request #80 from ButteredCats/fix_multiple_images
Fix multiple Reddit preview links becoming the same
2024-04-07 11:58:51 -04:00
858299c861 Add test for rewriting multiple preview links 2024-04-07 11:24:18 -04:00
75f5c6668c Merge pull request #77 from EMarshal/main
Update PUSHSHIFT_FRONTEND examples to undelete.pullpush.io
2024-04-07 10:53:40 -04:00
e6b9a2e426 Fix anchor tags scrolling to the wrong place when fixed navbar is enabled (#82) 2024-04-07 10:52:59 -04:00
d4a2b3edc6 Add SVG logo (#84)
* Add SVG version of logo

* Add PNG generated from SVG
2024-04-07 10:51:33 -04:00
4f0b29f930 Fix failing checks 2024-03-30 18:53:46 -04:00
4e2648280d Fix multiple Reddit preview links becoming the same 2024-03-30 18:36:28 -04:00
35ae71302f Update PUSHSHIFT_FRONTEND examples to undelete.pullpush.io
Matches the change in 3592728
2024-03-28 17:09:12 -06:00
e79242c9e7 v0.31.2 2024-03-04 20:20:23 -05:00
da581cb79b Update deps (GHSA-r8w9-5wcg-vfj7) 2024-03-04 20:02:58 -05:00
3f4526debe Container pipeline overhaul (#59)
* Simplified docker image building

* minor name fix

* Optimize container pipeline

* Change config to redlib-org account

* Added README push to Quay.io

* Fixes
2024-03-04 20:02:06 -05:00
5de171b13a Make HLS support checking more robust (#61) 2024-03-04 19:59:54 -05:00
22910956db Overhaul README.md (#56)
* Use GitHub Flavored Markdown for admonitions

* Add syntax highlighting to code blocks

* Minor raw formatting changes

* Add 'Metric' header to PageSpeed Insights table

* Fix typo

* Minor wording changes

* Add link to launchd page on Wikipedia

* Overhaul README.md

* Add table of contents

* Reorganise About section

* Edit PageSpeed table

* Add instructions for streaming container logs

* Rework Instances section

* Add note about Compose plugin installation

* Add link to instance monitoring

* Move info on architecture specific images into an admonition

* Change NGINX reverse proxy admonition to important

* Change Redlib external links admonition to a note

* Edit section headings

* Fix anchor link under Deployment section

* Add comparison with Libreddit

* Update ToC
2024-02-15 15:58:33 -05:00
94ada2b10c Remove seccomp profile from yml (#34) 2024-02-12 14:43:36 -05:00
1f246c956d Rename compose.dev.yml to compose.dev.yaml (#52)
Minor typo
2024-02-12 14:13:48 -05:00
b7778d5f95 Update dev compose yaml 2024-02-12 13:37:11 -05:00
f507fcfcf8 Expand Docker documentation and allow using .env (#49)
Fix seccomp error for Docker Compose
2024-02-12 13:34:02 -05:00
c6030064f1 Added video quality feature (#43)
* Fixed docker compose errors

* Added quality selector

* Remove log statement

Co-authored-by: Matthew Esposito <matt@matthew.science>

* Show kbps in quality

Co-authored-by: Matthew Esposito <matt@matthew.science>

* Make highest quality default

* Add styling, default option to highest

---------

Co-authored-by: Matthew Esposito <matt@matthew.science>
2024-02-06 15:27:23 -05:00
fe6123e05f Update docker-compose.yml to remove duplicate 'security_opt' (#45)
* Update docker-compose.yml to remove duplicate 'security_opt'

Docker compose complains with "mapping key "security_opt" already defined"

* Update and rename docker-compose.yml to compose.yaml

`version:` is deprecated:

https://docs.docker.com/compose/compose-file/04-version-and-name/

compose.yaml is the current standard:

https://docs.docker.com/contribute/style/terminology/#composeyaml
2024-02-06 12:14:37 -05:00
3bb5dc5f3e Update dependencies (Hyper v1.x) (#39)
* Cargo update

* Update major non-breaking changes

* Add deprecation feature-flags to hyper v0.14

* Semi-upgrade hyper-rustls

* Revert deprecated warnings
2024-02-06 08:01:59 -05:00
35927287f1 Replace www.unddit.com with undelete.pullpush.io (#42)
* Replace www.unddit.com with undelete.pullpush.io

* Update comment

---------

Co-authored-by: Matthew Esposito <matt@matthew.science>
2024-02-05 16:50:09 -05:00
469d0994f1 Handle errors from reddit (#35)
* Fix error handling logic

A 401 code is still an Ok(<...>) response

* Fix json key

* Run `cargo fmt`
2024-02-02 14:53:15 -05:00
99097da6b8 Remove pedantic clippy's 2024-01-28 13:36:27 -05:00
3d2c936a9e Refresh OAuth on 401 only (#33) 2024-01-28 09:28:42 -05:00
03e267f02e Fix pedantic clippy 2024-01-27 23:34:23 -05:00
6c2579cda9 Add check for unauthorized - refresh token 2024-01-27 23:31:21 -05:00
d0c5a1d93a Merge pull request #30 from tmak2002/main
compose: use official image instead building image
2024-01-27 13:36:43 -05:00
119b661639 add developmemt docker compose file 2024-01-26 17:06:30 +01:00
a8e2430e34 use official image 2024-01-26 17:03:56 +01:00
e8c257f801 Update README.md (fix #31) 2024-01-24 14:34:02 -05:00
ea3d248766 Update oauth_resources.rs 2024-01-24 14:30:17 -05:00
5604786146 Add new hls.js version, add script for updating (fixes #29) 2024-01-24 14:26:52 -05:00
9f9ae45f6e Add many Clippy's, fix many Clippy's 2024-01-19 20:16:17 -05:00
3e459f5415 Cargo update - fix possible DoS 2024-01-19 19:06:47 -05:00
95373f8261 More succinct fix to header parsing 2024-01-19 19:06:05 -05:00
3609564db0 Add error logging when rendering the Error page 2024-01-19 19:00:13 -05:00
fcde6ff689 Fix client.rs - properly return Err on invalid header (fix #28) 2024-01-19 18:58:08 -05:00
0f148c58d3 Merge pull request #21 from dethos/patch-1
Fix app.json
2024-01-12 16:56:59 -05:00
b1ef598f3c Update README.md to remove old referenced links 2024-01-12 16:53:40 -05:00
825e38b25f fix app.json
Remove a comma that was left behind and made the JSON content invalid.
2024-01-12 18:38:56 +00:00
f50872a88c Merge pull request #18 from ButteredCats/fix_footer
Less buggy solution to solving the floating footer problem
2024-01-07 16:54:50 -05:00
a445759a69 Be slightly smarter with options, fix footer being 30px above or below bottom on short posts 2024-01-06 16:53:58 -05:00
b578b717d7 Less buggy solution to solving the floating footer problem 2024-01-06 03:49:38 +00:00
78e51eb11f Merge pull request #17 from ButteredCats/fix_footer
Fix floating footer with "Keep navbar fixed" off
2024-01-03 21:31:26 -05:00
5d8529d6bb Fix floating footer with "Keep navbar fixed" off 2024-01-03 21:26:56 -05:00
66 changed files with 2258 additions and 1252 deletions

56
.env.example Normal file
View File

@ -0,0 +1,56 @@
# Redlib configuration
# See the Configuration section of the README for a more detailed explanation of these settings.
# Instance-specific settings
# Enable SFW-only mode for the instance
REDLIB_SFW_ONLY=off
# Set a banner message for the instance
REDLIB_BANNER=
# Disable search engine indexing
REDLIB_ROBOTS_DISABLE_INDEXING=off
# Set the Pushshift frontend for "removed" links
REDLIB_PUSHSHIFT_FRONTEND=undelete.pullpush.io
# Default user settings
# Set the default theme (options: system, light, dark, black, dracula, nord, laserwave, violet, gold, rosebox, gruvboxdark, gruvboxlight)
REDLIB_DEFAULT_THEME=system
# Set the default mascot
REDLIB_DEFAULT_MASCOT=none
# Set the default front page (options: default, popular, all)
REDLIB_DEFAULT_FRONT_PAGE=default
# Set the default layout (options: card, clean, compact)
REDLIB_DEFAULT_LAYOUT=card
# Enable wide mode by default
REDLIB_DEFAULT_WIDE=off
# Set the default post sort method (options: hot, new, top, rising, controversial)
REDLIB_DEFAULT_POST_SORT=hot
# Set the default comment sort method (options: confidence, top, new, controversial, old)
REDLIB_DEFAULT_COMMENT_SORT=confidence
# Enable blurring Spoiler content by default
REDLIB_DEFAULT_BLUR_SPOILER=off
# Enable showing NSFW content by default
REDLIB_DEFAULT_SHOW_NSFW=off
# Enable blurring NSFW content by default
REDLIB_DEFAULT_BLUR_NSFW=off
# Enable HLS video format by default
REDLIB_DEFAULT_USE_HLS=off
# Enable audio+video downloads with ffmpeg.wasm
REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
# Hide HLS notification by default
REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
# Disable autoplay videos by default
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
REDLIB_DEFAULT_SUBSCRIPTIONS=
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
REDLIB_DEFAULT_FILTERS=
# Hide awards by default
REDLIB_DEFAULT_HIDE_AWARDS=off
# Hide sidebar and summary
REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY=off
# Disable the confirmation before visiting Reddit
REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
# Hide score by default
REDLIB_DEFAULT_HIDE_SCORE=off
# Enable fixed navbar by default
REDLIB_DEFAULT_FIXED_NAVBAR=on

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

76
.github/workflows/build-artifacts.yaml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Release Build
on:
push:
paths-ignore:
- "*.md"
- "compose.*"
branches:
- "main"
release:
types: [published]
env:
CARGO_TERM_COLOR: always
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc
CC_aarch64_unknown_linux_musl: aarch64-linux-gnu-gcc
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc
CC_armv7_unknown_linux_musleabihf: arm-linux-gnueabihf-gcc
jobs:
build:
name: Rust project - latest
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-musl
- armv7-unknown-linux-musleabihf
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends musl-tools
- if: matrix.target == 'armv7-unknown-linux-musleabihf'
run: |
sudo apt update
sudo apt install -y gcc-arm-linux-gnueabihf musl-tools
- if: matrix.target == 'aarch64-unknown-linux-musl'
run: |
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu musl-tools
- name: Versions
id: version
run: echo "VERSION=$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')" >> "$GITHUB_OUTPUT"
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Package release
run: tar czf redlib-${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release/ redlib
- name: Upload release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
draft: true
files: |
redlib-${{ matrix.target }}.tar.gz
body: |
- ${{ github.event.head_commit.message }} ${{ github.sha }}
generate_release_notes: true

View File

@ -1,44 +1,112 @@
name: Docker Build name: Container build
on: on:
push: workflow_run:
paths-ignore: workflows: ["Release Build"]
- "**.md" types:
branches: - completed
- 'main' env:
REGISTRY_IMAGE: quay.io/redlib/redlib
jobs: jobs:
build-docker: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false
matrix: matrix:
config: include:
- { platform: 'linux/amd64', tag: 'latest', dockerfile: 'Dockerfile' } - { platform: linux/amd64, target: x86_64-unknown-linux-musl}
- { platform: 'linux/arm64', tag: 'latest-arm', dockerfile: 'Dockerfile.arm' } - { platform: linux/arm64, target: aarch64-unknown-linux-musl}
- { platform: 'linux/arm/v7', tag: 'latest-armv7', dockerfile: 'Dockerfile.armv7' } - { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf}
steps: steps:
- name: Checkout sources -
uses: actions/checkout@v3 name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU -
uses: docker/setup-qemu-action@v2 name: Docker meta
id: meta
uses: docker/metadata-action@v5
with: with:
platforms: all images: ${{ env.REGISTRY_IMAGE }}
tags: |
- name: Set up Docker Buildx type=sha
id: buildx type=raw,value=latest,enable={{is_default_branch}}
uses: docker/setup-buildx-action@v2 -
with: name: Set up QEMU
version: latest uses: docker/setup-qemu-action@v3
-
- name: Login to Quay.io name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
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
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
file: Dockerfile
build-args: TARGET=${{ matrix.target }}
-
name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
-
name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
-
name: Download digests
uses: actions/download-artifact@v3
with:
name: digests
path: /tmp/digests
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=sha
type=raw,value=latest,enable={{is_default_branch}}
-
name: Login to Quay.io Container Registry
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
-
name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: push README to Quay.io - 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 }}
@ -47,13 +115,8 @@ jobs:
provider: quay provider: quay
readme_file: 'README.md' readme_file: 'README.md'
- name: Build and push -
uses: docker/build-push-action@v5 name: Inspect image
with: run: |
context: . docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
file: ./${{ matrix.config.dockerfile }}
platforms: ${{ matrix.config.platform }}
push: true
tags: quay.io/redlib/redlib:${{ matrix.config.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -30,9 +30,15 @@ jobs:
with: with:
toolchain: stable toolchain: stable
- name: Install musl-gcc
run: sudo apt-get install musl-tools
- name: Install cargo musl target
run: rustup target add x86_64-unknown-linux-musl
# Building actions # Building actions
- name: Build - name: Build
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-musl
- name: Versions - name: Versions
id: version id: version
@ -45,17 +51,17 @@ jobs:
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Calculate SHA512 checksum - name: Calculate SHA512 checksum
run: sha512sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha512 run: sha512sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha512
- name: Calculate SHA256 checksum - name: Calculate SHA256 checksum
run: sha256sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha256 run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
name: Upload a Build Artifact name: Upload a Build Artifact
with: with:
name: redlib name: redlib
path: | path: |
target/x86_64-unknown-linux-gnu/release/redlib target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512 redlib.sha512
redlib.sha256 redlib.sha256
@ -68,7 +74,7 @@ jobs:
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }} name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
draft: true draft: true
files: | files: |
target/x86_64-unknown-linux-gnu/release/redlib target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512 redlib.sha512
redlib.sha256 redlib.sha256
body: | body: |

6
.gitignore vendored
View File

@ -1,4 +1,10 @@
/target /target
.env .env
redlib.toml
# Idea Files # Idea Files
.idea/ .idea/
# nix files
.direnv/
result

View File

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

37
CREDITS
View File

@ -2,7 +2,9 @@
674Y3r <87250374+674Y3r@users.noreply.github.com> 674Y3r <87250374+674Y3r@users.noreply.github.com>
accountForIssues <52367365+accountForIssues@users.noreply.github.com> accountForIssues <52367365+accountForIssues@users.noreply.github.com>
Adrian Lebioda <adrianlebioda@gmail.com> Adrian Lebioda <adrianlebioda@gmail.com>
Akanksh Chitimalla <55909985+Akanksh12@users.noreply.github.com>
alefvanoon <53198048+alefvanoon@users.noreply.github.com> alefvanoon <53198048+alefvanoon@users.noreply.github.com>
Ales Lerch <13370338+axeII@users.noreply.github.com>
Alexandre Iooss <erdnaxe@crans.org> Alexandre Iooss <erdnaxe@crans.org>
alyaeanyx <alexandra.hollmeier@mailbox.org> alyaeanyx <alexandra.hollmeier@mailbox.org>
AndreVuillemot160 <84594011+AndreVuillemot160@users.noreply.github.com> AndreVuillemot160 <84594011+AndreVuillemot160@users.noreply.github.com>
@ -11,58 +13,90 @@ Artemis <51862164+artemislena@users.noreply.github.com>
arthomnix <35371030+arthomnix@users.noreply.github.com> arthomnix <35371030+arthomnix@users.noreply.github.com>
Arya K <73596856+gi-yt@users.noreply.github.com> Arya K <73596856+gi-yt@users.noreply.github.com>
Austin Huang <im@austinhuang.me> Austin Huang <im@austinhuang.me>
Ayaka <ayaka@kitty.community>
backfire-monism-net <development.0extl@simplelogin.com>
Basti <pred2k@users.noreply.github.com> Basti <pred2k@users.noreply.github.com>
Ben Sherman <bennettmsherman@gmail.com>
Ben Smith <37027883+smithbm2316@users.noreply.github.com> Ben Smith <37027883+smithbm2316@users.noreply.github.com>
beucismis <beucismis@tutamail.com>
BobIsMyManager <ahoumatt@yahoo.com> BobIsMyManager <ahoumatt@yahoo.com>
Butter Cat <butteredcats@protonmail.com>
Butter Cat <ButteredCats@protonmail.com>
Carbrex <95964955+Carbrex@users.noreply.github.com>
ccuser44 <68124053+ccuser44@users.noreply.github.com>
Connor Holloway <c.holloway314@outlook.com>
curlpipe <11898833+curlpipe@users.noreply.github.com> curlpipe <11898833+curlpipe@users.noreply.github.com>
dacousb <53299044+dacousb@users.noreply.github.com> dacousb <53299044+dacousb@users.noreply.github.com>
Daniel Nathan Gray <dng@disroot.org>
Daniel Valentine <Daniel-Valentine@users.noreply.github.com> Daniel Valentine <Daniel-Valentine@users.noreply.github.com>
Daniel Valentine <daniel@vielle.ws> Daniel Valentine <daniel@vielle.ws>
dbrennand <52419383+dbrennand@users.noreply.github.com> dbrennand <52419383+dbrennand@users.noreply.github.com>
Dean Sallinen <deza604@gmail.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com> Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com>
domve <domve@posteo.net> domve <domve@posteo.net>
Dyras <jevwmguf@duck.com> Dyras <jevwmguf@duck.com>
Edward <101938856+EdwardLangdon@users.noreply.github.com> Edward <101938856+EdwardLangdon@users.noreply.github.com>
Éli Marshal <835958+EMarshal@users.noreply.github.com>
elliot <75391956+ellieeet123@users.noreply.github.com> elliot <75391956+ellieeet123@users.noreply.github.com>
erdnaxe <erdnaxe@users.noreply.github.com> erdnaxe <erdnaxe@users.noreply.github.com>
Esmail EL BoB <github.defilable@simplelogin.co> Esmail EL BoB <github.defilable@simplelogin.co>
fawn <fawn@envs.net>
FireMasterK <20838718+FireMasterK@users.noreply.github.com> FireMasterK <20838718+FireMasterK@users.noreply.github.com>
George Roubos <cowkingdom@hotmail.com> George Roubos <cowkingdom@hotmail.com>
git-bruh <e817509a-8ee9-4332-b0ad-3a6bdf9ab63f@aleeas.com> git-bruh <e817509a-8ee9-4332-b0ad-3a6bdf9ab63f@aleeas.com>
gmnsii <95436780+gmnsii@users.noreply.github.com> gmnsii <95436780+gmnsii@users.noreply.github.com>
gmnsii <github.gmnsii@pm.me>
gmnsii <gmnsii@void.noreply>
Gonçalo Valério <dethos@users.noreply.github.com>
guaddy <67671414+guaddy@users.noreply.github.com> guaddy <67671414+guaddy@users.noreply.github.com>
Harsh Mishra <erbeusgriffincasper@gmail.com> Harsh Mishra <erbeusgriffincasper@gmail.com>
hinto.janai <hinto.janai@protonmail.com>
igna <igna@intent.cool> igna <igna@intent.cool>
imabritishcow <bcow@protonmail.com> imabritishcow <bcow@protonmail.com>
invakid404 <invakid404@riseup.net>
İsmail Karslı <ismail@karsli.net>
Johannes Schleifenbaum <johannes@js-webcoding.de> Johannes Schleifenbaum <johannes@js-webcoding.de>
Jonathan Dahan <git@jonathan.is>
Josiah <70736638+fres7h@users.noreply.github.com> Josiah <70736638+fres7h@users.noreply.github.com>
JPyke3 <pyke.jacob1@gmail.com> JPyke3 <pyke.jacob1@gmail.com>
Kavin <20838718+FireMasterK@users.noreply.github.com> Kavin <20838718+FireMasterK@users.noreply.github.com>
Kazi <kzshantonu@users.noreply.github.com> Kazi <kzshantonu@users.noreply.github.com>
Kieran <42723993+EnderDev@users.noreply.github.com> Kieran <42723993+EnderDev@users.noreply.github.com>
Kieran <kieran@dothq.co> Kieran <kieran@dothq.co>
Kirk1984 <christoph-m@posteo.de>
kuanhulio <66286575+kuanhulio@users.noreply.github.com>
Kyle Roth <kylrth@gmail.com> Kyle Roth <kylrth@gmail.com>
laazyCmd <laazy.pr00gramming@protonmail.com> laazyCmd <laazy.pr00gramming@protonmail.com>
Laurențiu Nicola <lnicola@users.noreply.github.com> Laurențiu Nicola <lnicola@users.noreply.github.com>
Lena <102762572+MarshDeer@users.noreply.github.com> Lena <102762572+MarshDeer@users.noreply.github.com>
Leopardus <leopardus3@pm.me>
Macic <46872282+Macic-Dev@users.noreply.github.com> Macic <46872282+Macic-Dev@users.noreply.github.com>
Mario A <10923513+Midblyte@users.noreply.github.com> Mario A <10923513+Midblyte@users.noreply.github.com>
Márton <marton2@gmail.com>
Mathew Davies <ThePixelDeveloper@users.noreply.github.com>
Matthew Crossman <matt@crossman.page> Matthew Crossman <matt@crossman.page>
Matthew E <matt@matthew.science> Matthew E <matt@matthew.science>
Matthew Esposito <matt@matthew.science> Matthew Esposito <matt@matthew.science>
Mennaruuk <52135169+Mennaruuk@users.noreply.github.com> Mennaruuk <52135169+Mennaruuk@users.noreply.github.com>
Midou36O <midou@midou.dev>
mikupls <93015331+mikupls@users.noreply.github.com> mikupls <93015331+mikupls@users.noreply.github.com>
Myzel394 <50424412+Myzel394@users.noreply.github.com>
Nainar <nainar.mb@gmail.com> Nainar <nainar.mb@gmail.com>
Nathan Moos <moosingin3space@gmail.com> Nathan Moos <moosingin3space@gmail.com>
Nazar <63452145+Tokarak@users.noreply.github.com>
Nicholas Christopher <nchristopher@tuta.io> Nicholas Christopher <nchristopher@tuta.io>
Nick Lowery <ClockVapor@users.noreply.github.com> Nick Lowery <ClockVapor@users.noreply.github.com>
Nico <github@dr460nf1r3.org> Nico <github@dr460nf1r3.org>
NKIPSC <15067635+NKIPSC@users.noreply.github.com> NKIPSC <15067635+NKIPSC@users.noreply.github.com>
nohoster <136514837+nohoster@users.noreply.github.com>
o69mar <119129086+o69mar@users.noreply.github.com> o69mar <119129086+o69mar@users.noreply.github.com>
obeho <71698631+obeho@users.noreply.github.com> obeho <71698631+obeho@users.noreply.github.com>
obscurity <z@x4.pm> obscurity <z@x4.pm>
Om G <34579088+OxyMagnesium@users.noreply.github.com> Om G <34579088+OxyMagnesium@users.noreply.github.com>
Ondřej Pešek <iTzBoboCz@users.noreply.github.com>
perennial <mail@perennialte.ch>
Peter Sawyer <petersawyer314@gmail.com>
pin <90570748+0323pin@users.noreply.github.com> pin <90570748+0323pin@users.noreply.github.com>
potatoesAreGod <118043038+potatoesAreGod@users.noreply.github.com> potatoesAreGod <118043038+potatoesAreGod@users.noreply.github.com>
RiversideRocks <59586759+RiversideRocks@users.noreply.github.com> RiversideRocks <59586759+RiversideRocks@users.noreply.github.com>
@ -86,11 +120,14 @@ TheCultLeader666 <65368815+TheCultLeader666@users.noreply.github.com>
TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com> TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com>
The TwilightBlood <hwengerstickel@protonmail.com> The TwilightBlood <hwengerstickel@protonmail.com>
tirz <36501933+tirz@users.noreply.github.com> tirz <36501933+tirz@users.noreply.github.com>
tmak2002 <torben@tmak2002.dev>
Tokarak <63452145+Tokarak@users.noreply.github.com> Tokarak <63452145+Tokarak@users.noreply.github.com>
Tsvetomir Bonev <invakid404@riseup.net> Tsvetomir Bonev <invakid404@riseup.net>
Vivek <vivek@revankar.net>
Vladislav Nepogodin <nepogodin.vlad@gmail.com> Vladislav Nepogodin <nepogodin.vlad@gmail.com>
Walkx <walkxnl@gmail.com> Walkx <walkxnl@gmail.com>
Wichai <1482605+Chengings@users.noreply.github.com> Wichai <1482605+Chengings@users.noreply.github.com>
wsy2220 <wsy@dogben.com> wsy2220 <wsy@dogben.com>
xatier <xatierlike@gmail.com> xatier <xatierlike@gmail.com>
Yaroslav Chvanov <yaroslav.chvanov@gmail.com>
Zach <72994911+zachjmurphy@users.noreply.github.com> Zach <72994911+zachjmurphy@users.noreply.github.com>

735
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[package] [package]
name = "redlib" name = "redsunlib"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0" license = "AGPL-3.0"
repository = "https://github.com/redlib-org/redlib" repository = "https://git.stardust.wtf/iridium/redlib"
version = "0.31.0" version = "0.35.1"
authors = [ authors = [
"Matthew Esposito <matt+cargo@matthew.science>", "Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>", "spikecodes <19519553+spikecodes@users.noreply.github.com>",
@ -11,8 +11,8 @@ authors = [
edition = "2021" edition = "2021"
[dependencies] [dependencies]
askama = { version = "0.11.1", default-features = false } askama = { version = "0.12.1", default-features = false }
cached = { version = "0.46.1", features = ["async"] } cached = { version = "0.51.3", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [ clap = { version = "4.4.11", default-features = false, features = [
"std", "std",
"env", "env",
@ -20,9 +20,9 @@ clap = { version = "4.4.11", default-features = false, features = [
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 = "1.13.0" futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] } hyper = { version = "0.14.28", features = ["full"] }
hyper-rustls = "0.24.2" 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.108"
@ -31,13 +31,13 @@ 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 = "3.4.0", features = ["std"] } brotli = { version = "6.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"
build_html = "2.4.0" build_html = "2.4.0"
uuid = { version = "1.6.1", features = ["v4"] } uuid = { version = "1.6.1", features = ["v4"] }
base64 = "0.21.5" base64 = "0.22.1"
fastrand = "2.0.1" fastrand = "2.0.1"
log = "0.4.20" log = "0.4.20"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"

View File

@ -1,19 +1,17 @@
####################################################################################################
## Builder ## Builder
####################################################################################################
FROM rust:alpine AS builder FROM rust:alpine AS builder
RUN apk add --no-cache musl-dev RUN apk add --no-cache musl-dev git
WORKDIR /redlib WORKDIR /redsunlib
COPY . . COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --release RUN cargo build --target x86_64-unknown-linux-musl --release
####################################################################################################
## Final image ## Final image
####################################################################################################
FROM alpine:latest FROM alpine:latest
# Import ca-certificates from builder # Import ca-certificates from builder
@ -21,16 +19,16 @@ COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=builder /etc/ssl/certs /etc/ssl/certs COPY --from=builder /etc/ssl/certs /etc/ssl/certs
# Copy our build # Copy our build
COPY --from=builder /redlib/target/x86_64-unknown-linux-musl/release/redlib /usr/local/bin/redlib COPY --from=builder /redsunlib/target/x86_64-unknown-linux-musl/release/redsunlib /usr/local/bin/redsunlib
# Use an unprivileged user. # Use an unprivileged user.
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib RUN adduser --home /nonexistent --no-create-home --disabled-password redsunlib
USER redlib USER redsunlib
# Tell Docker to expose port 8080 # Tell Docker to expose port 8080
EXPOSE 8080 EXPOSE 8080
# Run a healthcheck every minute to make sure redlib is functional # Run a healthcheck every minute to make sure redsunlib is functional
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
CMD ["redlib"] CMD ["redsunlib"]

View File

@ -1,45 +0,0 @@
####################################################################################################
## Builder
####################################################################################################
FROM rust:alpine AS builder
RUN apk add --no-cache g++ git
WORKDIR /usr/src/redlib
# cache dependencies in their own layer
COPY Cargo.lock Cargo.toml .
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo install --config net.git-fetch-with-cli=true --path . && rm -rf ./src
COPY . .
# net.git-fetch-with-cli is specified in order to prevent a potential OOM kill
# in low memory environments. See:
# https://users.rust-lang.org/t/cargo-uses-too-much-memory-being-run-in-qemu/76531
# This is tracked under issue #641. This also requires us to install git in the
# builder.
RUN cargo install --config net.git-fetch-with-cli=true --path .
####################################################################################################
## Final image
####################################################################################################
FROM alpine:latest
# Import ca-certificates from builder
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
# Copy our build
COPY --from=builder /usr/local/cargo/bin/redlib /usr/local/bin/redlib
# Use an unprivileged user.
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
USER redlib
# Tell Docker to expose port 8080
EXPOSE 8080
# Run a healthcheck every minute to make sure redlib is functional
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
CMD ["redlib"]

View File

@ -1,43 +0,0 @@
####################################################################################################
## Builder
####################################################################################################
FROM --platform=$BUILDPLATFORM rust:slim AS builder
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc
ENV CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc
RUN apt-get update && apt-get -y install gcc-arm-linux-gnueabihf \
binutils-arm-linux-gnueabihf \
musl-tools
RUN rustup target add armv7-unknown-linux-musleabihf
WORKDIR /redlib
COPY . .
RUN cargo build --target armv7-unknown-linux-musleabihf --release
####################################################################################################
## Final image
####################################################################################################
FROM alpine:latest
# Import ca-certificates from builder
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
# Copy our build
COPY --from=builder /redlib/target/armv7-unknown-linux-musleabihf/release/redlib /usr/local/bin/redlib
# Use an unprivileged user.
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
USER redlib
# Tell Docker to expose port 8080
EXPOSE 8080
# Run a healthcheck every minute to make sure redlib is functional
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
CMD ["redlib"]

408
README.md
View File

@ -1,48 +1,71 @@
# Redlib <img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo">
> An alternative private front-end to Reddit # Redsunlib
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some <sup><sub>(minor)</sub></sup> function and cosmetic changes.
# ⚠️ Why do I get TOO MANY REQUESTS errors? ⚠️ <br>
#### As of July 12th, 2023, Redlib is currently not operational as Reddit's API changes, that were designed to kill third-party apps and content scrapers who don't pay [large fees](https://www.theverge.com/2023/5/31/23743993/reddit-apollo-client-api-cost), went into effect. [Read the full announcement here.](https://github.com/libreddit/libreddit/issues/840)
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png) ![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
### Disclaimer
There are rapid changes/features in this fork that can<sup>(will)</sup> change without notice. If you want to host this version, be aware that it's likely to break at some point. I still wouldn't recommend it in a production environment unless you know what you're doing. Or like living on the edge.......
> I would also like to thank the maintainers and contributors of both [Redlib](https://github.com/redlib-org/redlib) and [Libreddit](https://github.com/libreddit/libreddit) for all the work they did while I just added some low quality tacky features. ❤️
--- ---
**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit). ## Table of Contents
1. [Redsunlib](#redsunlib)
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety - [Disclaimer](#disclaimer)
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat 2. [Table of Contents](#table-of-contents)
- 🕵 Private: all requests are proxied through the server, including media 3. [Instances](#instances)
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit 4. [About](#about)
- [The Name](#the-name)
--- - [Built with](#built-with)
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
I appreciate any donations! Your support allows me to continue developing Redlib. - [Teddit](#teddit)
- [Libreddit](#libreddit)
<a href="https://www.buymeacoffee.com/spikecodes" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px" ></a> 5. [Comparison](#comparison)
<a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" style="height: 40px"></a> - [Speed](#speed)
- [Privacy](#privacy)
- [Reddit](#reddit)
**Bitcoin:** `bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y` - [Redlib](#redlib-1)
- [Server](#server)
**Monero:** `45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR` 6. [Deployment](#deployment)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [Docker CLI](#docker-cli)
- [Binary](#binary)
- [Running as a systemd service](#running-as-a-systemd-service)
- [Building from source](#building-from-source)
7. [Configuration](#configuration)
- [Instance settings](#instance-settings)
- [Default user settings](#default-user-settings)
--- ---
# Instances # Instances
🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!** > [!WARNING]
> 🔗 **Currently public Redsunlib instance are not available, consider using a [redlib](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) instance if you are not comfortable running your own**
[Follow this link](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) for an up-to-date table of instances in Markdown format. This list is also available as [a machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json). You are more than welcome to host an instance and submit an issue if you want it added. That is, if you've read the [Disclaimer](#disclaimer) and it's within your "personal risk tolerance." ;)
Both files are part of the [libreddit-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [libreddit-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
--- ---
# About # About
Find Redlib on 💬 [Matrix](https://matrix.to/#/#redlib:matrix.org), 🐋 [Quay.io](https://quay.io/repository/redlib/redlib), :octocat: [GitHub](https://github.com/redlib-org/redlib), and 🦊 [GitLab](https://gitlab.com/redlib/redlib). Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
## The Name
**Red sun** in the sky + Red**lib** = Redsunlib
And at the time, I was reading an excerpt from Mao Zedong, so the name seemed appropriate. But paradoxically named since Reddit is basically the sinophobia capital of the internet :/
## Built with ## Built with
@ -51,42 +74,51 @@ Find Redlib on 💬 [Matrix](https://matrix.to/#/#redlib:matrix.org), 🐋 [Quay
- [Askama](https://github.com/djc/askama) - Templating engine - [Askama](https://github.com/djc/askama) - Templating engine
- [Rustls](https://github.com/rustls/rustls) - TLS library - [Rustls](https://github.com/rustls/rustls) - TLS library
## Info ## How is it different from other Reddit front ends?
Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/libreddit/libreddit/issues). ### Teddit
## How does it compare to Teddit?
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Redlib into an even more polished product. Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Redlib into an even more polished product.
If you are looking to compare, the biggest differences I have noticed are: If you are looking to compare, the biggest differences I have noticed are:
- Redlib is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective. - Redlib is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
- Redlib is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation. - Redlib is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
### Libreddit
While originating as a fork of Libreddit, the name "Redlib" was adopted to avoid legal issues, as Reddit only allows the use of their name if structured as "XYZ For Reddit".
Several technical improvements have also been made, including:
- **OAuth token spoofing**: To circumvent rate limits imposed by Reddit, OAuth token spoofing is used to mimick the most common iOS and Android clients. While spoofing both iOS and Android clients was explored, only the Android client was chosen due to content restrictions when using an anonymous iOS client.
- **Token refreshing**: The authentication token is refreshed every 24 hours, emulating the behavior of the official Android app.
- **HTTP header mimicking**: Efforts are made to send along as many of the official app's headers as possible to reduce the likelihood of Reddit's crackdown on Redlib's requests.
--- ---
# Comparison # Comparison
This section outlines how Redlib compares to Reddit. This section outlines how Redlib compares to Reddit in terms of speed and privacy.
## Speed ## Speed
Lasted tested Nov 11, 2022. Last tested on January 12, 2024.
Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.dev/report?url=https%3A%2F%2Flibreddit.spike.codes%2F), [Reddit Report](https://pagespeed.web.dev/report?url=https://www.reddit.com)). Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.dev/report?url=https%3A%2F%2Fredlib.matthew.science%2F), [Reddit Report](https://pagespeed.web.dev/report?url=https://www.reddit.com)).
| | Redlib | Reddit | | Performance metric | Redlib | Reddit |
|------------------------|-------------|-----------| | ------------------- | -------- | --------- |
| Requests | 60 | 83 | | Speed Index | 0.6s | 1.9s |
| Speed Index | 2.0s | 10.4s | | Performance Score | 100% | 64% |
| Time to Interactive | **2.8s** | **12.4s** | | Time to Interactive | **2.8s** | **12.4s** |
## Privacy ## Privacy
### Reddit ### Reddit
**Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including: **Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including:
- IP address - IP address
- User-agent string - User-agent string
- Browser type - Browser type
@ -100,12 +132,14 @@ Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.de
- Search terms - Search terms
**Location:** The same privacy policy goes on to describe that location data may be collected through the use of: **Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
- GPS (consensual) - GPS (consensual)
- Bluetooth (consensual) - Bluetooth (consensual)
- Content associated with a location (consensual) - Content associated with a location (consensual)
- Your IP Address - Your IP Address
**Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding: **Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding:
- Authentication - Authentication
- Functionality - Functionality
- Analytics and Performance - Analytics and Performance
@ -119,156 +153,93 @@ For transparency, I hope to describe all the ways Redlib handles user privacy.
#### Server #### Server
* **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting. - **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
* **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data. - **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data.
#### Official instance (libreddit.spike.codes)
The official instance is hosted at https://libreddit.spike.codes.
* **Server:** The official instance runs a production binary, and thus logs nothing.
* **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic.
* **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
---
# Installation
## 1) Cargo
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
```
cargo install libreddit
```
## 2) Docker
Deploy the [Docker image](https://quay.io/repository/redlib/redlib) of Redlib:
```
docker pull quay.io/redlib/redlib
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib
```
Deploy using a different port (in this case, port 80):
```
docker pull quay.io/redlib/redlib
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib
```
To deploy on `arm64` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-arm`.
To deploy on `armv7` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-armv7`.
## 3) AUR
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
```
yay -S libreddit-git
```
## 4) NetBSD/pkgsrc
For NetBSD users, Redlib is available from the official repositories.
```
pkgin install libreddit
```
Or, if you prefer to build from source
```
cd /usr/pkgsrc/libreddit
make install
```
## 5) GitHub Releases
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest).
## 6) Replit/Heroku/Glitch
> **Warning**
> These are free hosting options, but they are *not* private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
[![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button-v2.svg)](https://glitch.com/edit/#!/remix/libreddit)
--- ---
# Deployment # Deployment
Once installed, deploy Redlib to `0.0.0.0:8080` by running: This section covers multiple ways of deploying Redlib. Using [Docker](#docker) is recommended for production.
For configuration options, see the [Configuration section](#Configuration).
## Docker
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
Docker images for Redsunlib are available at our [Gitea container registry](https://git.stardust.wtf/iridium/-/packages/container/redsunlib/latest), currently only with support for `amd64`, if you need `arm64`, or `armv7` platforms you can either build Redsunlib yourself or open an [issue](https://git.stardust.wtf/iridium/redsunlib/issues) :)
### Docker Compose
> [!IMPORTANT]
> These instructions assume the [Compose plugin](https://docs.docker.com/compose/migrate/#what-are-the-differences-between-compose-v1-and-compose-v2) has already been installed. If not, follow these [instructions on the Docker Docs](https://docs.docker.com/compose/install) for how to do so.
Copy `compose.yaml` and modify any relevant values (for example, the ports Redlib should listen on).
Start Redlib in detached mode (running in the background):
```bash
docker compose up -d
``` ```
Stream logs from the Redlib container:
```bash
docker logs -f redlib
```
### Docker CLI
Deploy Redlib:
```bash
docker pull git.stardust.wtf/iridium/redsunlib:latest
docker run -d --name redlib -p 8080:8080 git.stardust.wtf/iridium/redsunlib:latest
```
Deploy using a different port on the host (in this case, port 80):
```bash
docker pull git.stardust.wtf/iridium/redsunlib:latest
docker run -d --name redlib -p 80:8080 git.stardust.wtf/iridium/redsunlib:latest
```
If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`.
Stream logs from the Redlib container:
```bash
docker logs -f redlib
```
## Binary
Currently binaries are not supplied at this moment but will be at some point in the future but can be [built from source](#building-from-source)
Copy the binary to `/usr/bin`:
```bash
sudo cp ./redlib /usr/bin/redlib
```
Deploy Redlib to `0.0.0.0:8080`:
```bash
redlib redlib
``` ```
## Instance settings > [!IMPORTANT]
> If you're proxying Redlib through NGINX (see [issue #122](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853)), add
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. >
| Name | Possible values | Default value | Description |
|---------------------------|-----------------|------------------|-----------------------------------------------------------------------------------------------------------|
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `www.unddit.com` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
## Default User Settings
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value |
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
| `HIDE_SCORE` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
You can also configure Redlib with a configuration file. An example `redlib.toml` can be found below:
```toml
REDLIB_DEFAULT_WIDE = "on"
REDLIB_DEFAULT_USE_HLS = "on"
```
### Examples
```bash
REDLIB_DEFAULT_SHOW_NSFW=on redlib
```
```bash
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
```
## Proxying using NGINX
> **Note**
> If you're [proxying Redlib through an NGINX Reverse Proxy](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853), add
> ```nginx > ```nginx
> proxy_http_version 1.1; > proxy_http_version 1.1;
> ``` > ```
>
> to your NGINX configuration file above your `proxy_pass` line. > to your NGINX configuration file above your `proxy_pass` line.
## systemd ### Running as a systemd service
You can use the systemd service available in `contrib/redlib.service` You can use the systemd service available in `contrib/redlib.service`
(install it on `/etc/systemd/system/redlib.service`). (install it on `/etc/systemd/system/redlib.service`).
@ -287,18 +258,83 @@ guarantee nginx waits for this service to start. Edit
Before=nginx.service Before=nginx.service
``` ```
## launchd ## Building from source
If you are on macOS, you can use the launchd service available in `contrib/redlib.plist`. To deploy Redsunlib with changes not yet included in the latest release, you can build the application from source.
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`. ```bash
git clone https://git.stardust.wtf/iridium/redsunlib && cd redsunlib
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
## Building
```
git clone https://github.com/redlib-org/redlib
cd redlib
cargo run cargo run
``` ```
---
# Configuration
You can configure Redlib further using environment variables. For example:
```bash
REDLIB_DEFAULT_SHOW_NSFW=on redlib
```
```bash
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
```
You can also configure Redlib with a configuration file named `redlib.toml`. For example:
```toml
REDLIB_DEFAULT_WIDE = "on"
REDLIB_DEFAULT_USE_HLS = "on"
```
> [!NOTE]
> If you're deploying Redlib using the **Docker CLI or Docker Compose**, environment variables can be defined in a [`.env` file](https://docs.docker.com/compose/environment-variables/set-environment-variables/), allowing you to centralize and manage configuration in one place.
>
> To configure Redlib using a `.env` file, copy the `.env.example` file to `.env` and edit it accordingly.
>
> If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example:
>
> ```bash
> docker run -d --name redlib -p 8080:8080 --env-file .env git.stardust.wtf/iridium/redsunlib:latest
> ```
>
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
## Instance settings
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value | Description |
| ------------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
## Default user settings
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "icebergDark"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `FFMPEG_VIDEO_DOWNLOADS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
| `HIDE_SCORE` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |

View File

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

View File

@ -1,26 +1,26 @@
# docker-compose -f docker-compose.dev.yml up -d
version: "3.8" version: "3.8"
services: services:
web: redsunlib:
build: . build: .
restart: always restart: always
container_name: "redlib" container_name: "redsunlib"
ports: ports:
- 8080:8080 - 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
user: nobody user: nobody
read_only: true read_only: true
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
# - seccomp=seccomp-redsunlib.json
cap_drop: cap_drop:
- ALL - ALL
networks: networks:
- redlib - redsunlib
security_opt:
- seccomp="seccomp-redlib.json"
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"] test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
interval: 5m interval: 5m
timeout: 3s timeout: 3s
networks: networks:
redlib: redsunlib:

24
compose.yaml Normal file
View File

@ -0,0 +1,24 @@
services:
redsunlib:
image: git.stardust.wtf/iridium/redsunlib:latest
restart: always
container_name: "redsunlib"
ports:
- 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
user: nobody
read_only: true
security_opt:
- no-new-privileges:true
# - seccomp=seccomp-redsunlib.json
cap_drop:
- ALL
env_file: .env
networks:
- redsunlib
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
interval: 5m
timeout: 3s
networks:
redsunlib:

View File

@ -1,14 +1,17 @@
ADDRESS=0.0.0.0 ADDRESS=0.0.0.0
PORT=12345 PORT=12345
#REDLIB_DEFAULT_THEME=default #REDLIB_DEFAULT_THEME=default
#REDLIB_DEFAULT_MASCOT=none
#REDLIB_DEFAULT_FRONT_PAGE=default #REDLIB_DEFAULT_FRONT_PAGE=default
#REDLIB_DEFAULT_LAYOUT=card #REDLIB_DEFAULT_LAYOUT=card
#REDLIB_DEFAULT_WIDE=off #REDLIB_DEFAULT_WIDE=off
#REDLIB_DEFAULT_POST_SORT=hot #REDLIB_DEFAULT_POST_SORT=hot
#REDLIB_DEFAULT_COMMENT_SORT=confidence #REDLIB_DEFAULT_COMMENT_SORT=confidence
#REDLIB_DEFAULT_BLUR_SPOILER=off
#REDLIB_DEFAULT_SHOW_NSFW=off #REDLIB_DEFAULT_SHOW_NSFW=off
#REDLIB_DEFAULT_BLUR_NSFW=off #REDLIB_DEFAULT_BLUR_NSFW=off
#REDLIB_DEFAULT_USE_HLS=off #REDLIB_DEFAULT_USE_HLS=off
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off #REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off #REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3) #REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)

106
flake.lock generated Normal file
View File

@ -0,0 +1,106 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1717025063,
"narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=",
"owner": "ipetkov",
"repo": "crane",
"rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717112898,
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1717121863,
"narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

71
flake.nix Normal file
View File

@ -0,0 +1,71 @@
{
description = "Redlib: Private front-end for Reddit";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
};
outputs = { nixpkgs, crane, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
inherit (pkgs) lib;
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
targets = [ "x86_64-unknown-linux-musl" ];
};
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
src = lib.cleanSourceWith {
src = craneLib.path ./.;
filter = path: type:
(lib.hasInfix "/templates/" path) ||
(lib.hasInfix "/static/" path) ||
(craneLib.filterCargoSources path type);
};
redlib = craneLib.buildPackage {
inherit src;
strictDeps = true;
doCheck = false;
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
};
in
{
checks = {
my-crate = redlib;
};
packages.default = redlib;
packages.docker = pkgs.dockerTools.buildImage {
name = "quay.io/redlib/redlib";
tag = "latest";
created = "now";
copyToRoot = with pkgs.dockerTools; [ caCertificates fakeNss ];
config.Cmd = "${redlib}/bin/redlib";
};
});
}

31
scripts/load_test.py Normal file
View File

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

18
scripts/update_hls_js.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
cd "$(dirname "$0")"
LATEST_TAG=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases/latest | jq -r '.tag_name')
if [[ -z "$LATEST_TAG" || "$LATEST_TAG" == "null" ]]; then
echo "Failed to fetch the latest release tag from GitHub."
exit 1
fi
LICENSE="// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
// @source https://github.com/video-dev/hls.js/tree/$LATEST_TAG"
echo "$LICENSE" > ../static/hls.min.js
curl -s https://cdn.jsdelivr.net/npm/hls.js@${LATEST_TAG}/dist/hls.min.js >> ../static/hls.min.js
echo "Update complete. The latest hls.js (${LATEST_TAG}) has been saved to static/hls.min.js."

View File

@ -5,22 +5,30 @@ use hyper::client::HttpConnector;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri}; use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector; use hyper_rustls::HttpsConnector;
use libflate::gzip; use libflate::gzip;
use log::{error, trace, warn};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use percent_encoding::{percent_encode, CONTROLS}; use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value; use serde_json::Value;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicU16, Ordering::SeqCst};
use std::{io, result::Result}; use std::{io, result::Result};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::dbg_msg; use crate::dbg_msg;
use crate::oauth::{token_daemon, Oauth}; use crate::oauth::{force_refresh_token, token_daemon, Oauth};
use crate::server::RequestExt; 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";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build(); let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.expect("No native root certificates found")
.https_only()
.enable_http1()
.build();
client::Client::builder().build(https) client::Client::builder().build(https)
}); });
@ -30,6 +38,8 @@ pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
RwLock::new(client) RwLock::new(client)
}); });
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
/// 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`.
/// ///
@ -56,7 +66,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// If Reddit responds with a 301, then the path is redirected. // If Reddit responds with a 301, then the path is redirected.
301 => match res.headers().get(header::LOCATION) { 301 => match res.headers().get(header::LOCATION) {
Some(val) => { Some(val) => {
let original = val.to_str().unwrap(); let Ok(original) = val.to_str() else {
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
@ -88,12 +100,12 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
} }
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> { pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default()); let mut url = format!("{format}?{}", req.uri().query().unwrap_or_default());
// For each parameter in request // For each parameter in request
for (name, value) in req.params().iter() { for (name, value) in &req.params() {
// Fill the parameter value in the url // Fill the parameter value in the url
url = url.replace(&format!("{{{}}}", name), value); url = url.replace(&format!("{{{name}}}"), value);
} }
stream(&url, &req).await stream(&url, &req).await
@ -101,12 +113,12 @@ pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, S
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> { async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
// First parameter is target URL (mandatory). // First parameter is target URL (mandatory).
let 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::Client<_, hyper::Body> = CLIENT.clone(); let client: Client<_, Body> = CLIENT.clone();
let mut builder = Request::get(uri); let mut builder = Request::get(parsed_uri);
// Copy useful headers from original request // Copy useful headers from original request
for &key in &["Range", "If-Modified-Since", "Cache-Control"] { for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
@ -152,15 +164,15 @@ fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, S
request(&Method::HEAD, path, false, quarantine) request(&Method::HEAD, path, false, quarantine)
} }
/// 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) -> 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!("{REDDIT_URL_BASE}{path}");
// Construct the hyper client from the HTTPS connector. // Construct the hyper client from the HTTPS connector.
let client: client::Client<_, hyper::Body> = CLIENT.clone(); let client: Client<_, Body> = CLIENT.clone();
let (token, vendor_id, device_id, user_agent, loid) = { let (token, vendor_id, device_id, user_agent, loid) = {
let client = block_on(OAUTH_CLIENT.read()); let client = block_on(OAUTH_CLIENT.read());
@ -172,6 +184,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
) )
}; };
// Build request to Reddit. When making a GET, request gzip compression. // Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.) // (Reddit doesn't do brotli yet.)
let builder = Request::builder() let builder = Request::builder()
@ -182,7 +195,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.header("X-Reddit-Device-Id", device_id) .header("X-Reddit-Device-Id", device_id)
.header("x-reddit-loid", loid) .header("x-reddit-loid", loid)
.header("Host", "oauth.reddit.com") .header("Host", "oauth.reddit.com")
.header("Authorization", &format!("Bearer {}", token)) .header("Authorization", &format!("Bearer {token}"))
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" }) .header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5") .header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
@ -225,7 +238,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// //
// 2. Percent-encode the path. // 2. Percent-encode the path.
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string(); let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string();
format!("{}{}raw_json=1", new_path, if new_path.contains('?') { "&" } else { "?" }) format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
}) })
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
@ -298,59 +311,99 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
#[cached(size = 100, time = 30, result = true)] #[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> { pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// Closure to quickly build errors // Closure to quickly build errors
let err = |msg: &str, e: String| -> Result<Value, String> { let err = |msg: &str, e: String, path: String| -> Result<Value, String> {
// eprintln!("{} - {}: {}", url, msg, e); // eprintln!("{} - {}: {}", url, msg, e);
Err(format!("{}: {}", msg, e)) Err(format!("{msg}: {e} | {path}"))
}; };
// First, handle rolling over the OAUTH_CLIENT if need be.
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
if current_rate_limit < 10 {
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
tokio::spawn(force_refresh_token());
}
// Fetch the url... // Fetch the url...
match reddit_get(path.clone(), quarantine).await { match reddit_get(path.clone(), quarantine).await {
Ok(response) => { Ok(response) => {
let status = response.status(); let status = response.status();
// Ratelimit remaining
if let Some(Ok(remaining)) = response.headers().get("x-ratelimit-remaining").map(|val| val.to_str()) {
trace!("Ratelimit remaining: {}", remaining);
if let Ok(remaining) = remaining.parse::<f32>().map(|f| f.round() as u16) {
OAUTH_RATELIMIT_REMAINING.store(remaining, SeqCst);
} else {
warn!("Failed to parse rate limit {remaining} from header.");
}
}
// Ratelimit used
if let Some(Ok(used)) = response.headers().get("x-ratelimit-used").map(|val| val.to_str()) {
trace!("Ratelimit used: {}", used);
}
// Ratelimit reset
let reset = if let Some(Ok(reset)) = response.headers().get("x-ratelimit-reset").map(|val| val.to_str()) {
trace!("Ratelimit reset: {}", reset);
Some(reset.to_string())
} else {
None
};
// asynchronously aggregate the chunks of the body // asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await { match hyper::body::aggregate(response).await {
Ok(body) => { Ok(body) => {
let has_remaining = body.has_remaining();
if !has_remaining {
return match reset {
Some(val) => Err(format!("Reddit rate limit exceeded. Will reset in: {val}")),
None => Err("Reddit rate limit exceeded".to_string()),
};
}
// Parse the response from Reddit as JSON // Parse the response from Reddit as JSON
match serde_json::from_reader(body.reader()) { match serde_json::from_reader(body.reader()) {
Ok(value) => { Ok(value) => {
let json: Value = value; let json: Value = value;
// If Reddit returned an error // If Reddit returned an error
if json["error"].is_i64() { if json["error"].is_i64() {
Err( // OAuth token has expired; http status 401
json["reason"] if json["message"] == "Unauthorized" {
.as_str() error!("Forcing a token refresh");
.unwrap_or_else(|| { let () = force_refresh_token().await;
json["message"].as_str().unwrap_or_else(|| { return Err("OAuth token has expired. Please refresh the page!".to_string());
eprintln!("{}{} - Error parsing reddit error", REDDIT_URL_BASE, path); }
"Error parsing reddit error" Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
})
})
.to_string(),
)
} else { } else {
Ok(json) Ok(json)
} }
} }
Err(e) => { Err(e) => {
error!("Got an invalid response from reddit {e}. Status code: {status}");
if status.is_server_error() { if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string()) Err("Reddit is having issues, check if there's an outage".to_string())
} else { } else {
err("Failed to parse page JSON data", e.to_string()) err("Failed to parse page JSON data", e.to_string(), path)
} }
} }
} }
} }
Err(e) => err("Failed receiving body from Reddit", e.to_string()), Err(e) => err("Failed receiving body from Reddit", e.to_string(), path),
} }
} }
Err(e) => err("Couldn't send request to Reddit", e), Err(e) => err("Couldn't send request to Reddit", e, path),
} }
} }
#[cfg(test)]
static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL";
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_localization_popular() { async fn test_localization_popular() {
let val = json("/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL".to_string(), false).await.unwrap(); let val = json(POPULAR_URL.to_string(), false).await.unwrap();
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap()); assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
} }

View File

@ -9,15 +9,15 @@ use std::{env::var, fs::read_to_string};
// first request) and contains the instance settings. // first request) and contains the instance settings.
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load); pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
// This serves as the frontend for the Pushshift API - on removed comments, this URL will // This serves as the frontend for an archival API - on removed comments, this URL
// be the base of a link, to display removed content (on another site). // will be the base of a link, to display removed content (on another site).
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "www.unddit.com"; pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "undelete.pullpush.io";
/// Stores the configuration parsed from the environment variables and the /// Stores the configuration parsed from the environment variables and the
/// config file. `Config::Default()` contains None for each setting. /// config file. `Config::Default()` contains None for each setting.
/// When adding more config settings, add it to `Config::load`, /// When adding more config settings, add it to `Config::load`,
/// `get_setting_from_config`, both below, as well as /// `get_setting_from_config`, both below, as well as
/// instance_info::InstanceInfo.to_string(), README.md and app.json. /// `instance_info::InstanceInfo.to_string`(), README.md and app.json.
#[derive(Default, Serialize, Deserialize, Clone, Debug)] #[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
#[serde(rename = "REDLIB_SFW_ONLY")] #[serde(rename = "REDLIB_SFW_ONLY")]
@ -28,6 +28,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_THEME")] #[serde(alias = "LIBREDDIT_DEFAULT_THEME")]
pub(crate) default_theme: Option<String>, pub(crate) default_theme: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_MASCOT")]
#[serde(alias = "LIBREDDIT_DEFAULT_MASCOT")]
pub(crate) default_mascot: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_FRONT_PAGE")] #[serde(rename = "REDLIB_DEFAULT_FRONT_PAGE")]
#[serde(alias = "LIBREDDIT_DEFAULT_FRONT_PAGE")] #[serde(alias = "LIBREDDIT_DEFAULT_FRONT_PAGE")]
pub(crate) default_front_page: Option<String>, pub(crate) default_front_page: Option<String>,
@ -48,6 +52,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")] #[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
pub(crate) default_post_sort: Option<String>, pub(crate) default_post_sort: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_BLUR_SPOILER")]
#[serde(alias = "LIBREDDIT_DEFAULT_BLUR_SPOILER")]
pub(crate) default_blur_spoiler: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")] #[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")] #[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
pub(crate) default_show_nsfw: Option<String>, pub(crate) default_show_nsfw: Option<String>,
@ -60,6 +68,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")] #[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")]
pub(crate) default_use_hls: Option<String>, pub(crate) default_use_hls: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
#[serde(alias = "LIBREDDIT_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
pub(crate) default_ffmpeg_video_downloads: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION")] #[serde(rename = "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
pub(crate) default_hide_hls_notification: Option<String>, pub(crate) default_hide_hls_notification: Option<String>,
@ -68,6 +80,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")] #[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
pub(crate) default_hide_awards: Option<String>, pub(crate) default_hide_awards: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
pub(crate) default_hide_sidebar_and_summary: 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>,
@ -76,6 +92,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")] #[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
pub(crate) default_subscriptions: Option<String>, pub(crate) default_subscriptions: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_FILTERS")]
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>, pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -103,7 +123,7 @@ impl Config {
new_file.ok().and_then(|new_file| toml::from_str::<Self>(&new_file).ok()) new_file.ok().and_then(|new_file| toml::from_str::<Self>(&new_file).ok())
}; };
let config = load_config("redlib.toml").or(load_config("libreddit.toml")).unwrap_or_default(); let config = load_config("redlib.toml").or_else(|| load_config("libreddit.toml")).unwrap_or_default();
// This function defines the order of preference - first check for // This function defines the order of preference - first check for
// environment variables with "REDLIB", then check the legacy LIBREDDIT // environment variables with "REDLIB", then check the legacy LIBREDDIT
@ -112,23 +132,28 @@ impl Config {
// Return the first non-`None` value // Return the first non-`None` value
// If all are `None`, return `None` // If all are `None`, return `None`
let legacy_key = key.replace("REDLIB_", "LIBREDDIT_"); let legacy_key = key.replace("REDLIB_", "LIBREDDIT_");
var(key).ok().or(var(legacy_key).ok()).or(get_setting_from_config(key, &config)) var(key).ok().or_else(|| var(legacy_key).ok()).or_else(|| get_setting_from_config(key, &config))
}; };
Self { Self {
sfw_only: parse("REDLIB_SFW_ONLY"), sfw_only: parse("REDLIB_SFW_ONLY"),
default_theme: parse("REDLIB_DEFAULT_THEME"), default_theme: parse("REDLIB_DEFAULT_THEME"),
default_mascot: parse("REDLIB_DEFAULT_MASCOT"),
default_front_page: parse("REDLIB_DEFAULT_FRONT_PAGE"), default_front_page: parse("REDLIB_DEFAULT_FRONT_PAGE"),
default_layout: parse("REDLIB_DEFAULT_LAYOUT"), default_layout: parse("REDLIB_DEFAULT_LAYOUT"),
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"), default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
default_wide: parse("REDLIB_DEFAULT_WIDE"), default_wide: parse("REDLIB_DEFAULT_WIDE"),
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"), default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
default_blur_spoiler: parse("REDLIB_DEFAULT_BLUR_SPOILER"),
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"), default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"), default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"), default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS"), default_ffmpeg_video_downloads: parse("REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS"),
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_score: parse("REDLIB_DEFAULT_HIDE_SCORE"), default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"), default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"), default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
@ -141,18 +166,23 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
match name { match name {
"REDLIB_SFW_ONLY" => config.sfw_only.clone(), "REDLIB_SFW_ONLY" => config.sfw_only.clone(),
"REDLIB_DEFAULT_THEME" => config.default_theme.clone(), "REDLIB_DEFAULT_THEME" => config.default_theme.clone(),
"REDLIB_DEFAULT_MASCOT" => config.default_mascot.clone(),
"REDLIB_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(), "REDLIB_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(),
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(), "REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(), "REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(), "REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
"REDLIB_DEFAULT_BLUR_SPOILER" => config.default_blur_spoiler.clone(),
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(), "REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(), "REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(), "REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS" => config.default_ffmpeg_video_downloads.clone(),
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(), "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
"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_SCORE" => config.default_hide_score.clone(), "REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(), "REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(), "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(), "REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
@ -225,6 +255,12 @@ fn test_default_subscriptions() {
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into())); assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
} }
#[test]
#[sealed_test(env = [("REDLIB_DEFAULT_FILTERS", "news+bestof")])]
fn test_default_filters() {
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
}
#[test] #[test]
#[sealed_test] #[sealed_test]
fn test_pushshift() { fn test_pushshift() {

View File

@ -12,14 +12,14 @@ use std::borrow::ToOwned;
use std::collections::HashSet; use std::collections::HashSet;
use std::vec::Vec; use std::vec::Vec;
/// DuplicatesParams contains the parameters in the URL. /// `DuplicatesParams` contains the parameters in the URL.
struct DuplicatesParams { struct DuplicatesParams {
before: String, before: String,
after: String, after: String,
sort: String, sort: String,
} }
/// DuplicatesTemplate defines an Askama template for rendering duplicate /// `DuplicatesTemplate` defines an Askama template for rendering duplicate
/// posts. /// posts.
#[derive(Template)] #[derive(Template)]
#[template(path = "duplicates.html")] #[template(path = "duplicates.html")]
@ -59,7 +59,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Log the request in debugging mode // Log the request in debugging mode
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
dbg!(req.param("id").unwrap_or_default()); req.param("id").unwrap_or_default();
// Send the GET, and await JSON. // Send the GET, and await JSON.
match json(path, quarantined).await { match json(path, quarantined).await {
@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
} }
if have_after { if have_after {
before = "t3_".to_owned(); "t3_".clone_into(&mut before);
before.push_str(&duplicates[0].id); before.push_str(&duplicates[0].id);
} }
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
if have_before { if have_before {
// The next batch will need to start from one after the // The next batch will need to start from one after the
// last post in the current batch. // last post in the current batch.
after = "t3_".to_owned(); "t3_".clone_into(&mut after);
after.push_str(&duplicates[l - 1].id); after.push_str(&duplicates[l - 1].id);
// Here is where things get terrible. Notice that we // Here is where things get terrible. Notice that we
@ -182,14 +182,14 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
match json(new_path, true).await { match json(new_path, true).await {
Ok(response) => { Ok(response) => {
if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() { if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() {
before = "t3_".to_owned(); "t3_".clone_into(&mut before);
before.push_str(&duplicates[0].id); before.push_str(&duplicates[0].id);
} }
} }
Err(msg) => { Err(msg) => {
// Abort entirely if we couldn't get the previous // Abort entirely if we couldn't get the previous
// batch. // batch.
return error(req, msg).await; return error(req, &msg).await;
} }
} }
} else { } else {
@ -197,7 +197,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
} }
} }
template(DuplicatesTemplate { Ok(template(&DuplicatesTemplate {
params: DuplicatesParams { before, after, sort }, params: DuplicatesParams { before, after, sort },
post, post,
duplicates, duplicates,
@ -205,28 +205,28 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
url: req_url, url: req_url,
num_posts_filtered, num_posts_filtered,
all_posts_filtered, all_posts_filtered,
}) }))
} }
// Process error. // Process error.
Err(msg) => { Err(msg) => {
if msg == "quarantined" || msg == "gated" { if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub, msg) Ok(quarantine(&req, sub, &msg))
} else { } else {
error(req, msg).await error(req, &msg).await
} }
} }
} }
} }
// DUPLICATES // DUPLICATES
async fn parse_duplicates(json: &serde_json::Value, filters: &HashSet<String>) -> (Vec<Post>, u64, bool) { async fn parse_duplicates(json: &Value, filters: &HashSet<String>) -> (Vec<Post>, u64, bool) {
let post_duplicates: &Vec<Value> = &json["data"]["children"].as_array().map_or(Vec::new(), ToOwned::to_owned); let post_duplicates: &Vec<Value> = &json["data"]["children"].as_array().map_or(Vec::new(), ToOwned::to_owned);
let mut duplicates: Vec<Post> = Vec::new(); let mut duplicates: Vec<Post> = Vec::new();
// Process each post and place them in the Vec<Post>. // Process each post and place them in the Vec<Post>.
for val in post_duplicates.iter() { for val in post_duplicates {
let post: Post = parse_post(val).await; let post: Post = parse_post(val).await;
duplicates.push(post); duplicates.push(post);
} }

View File

@ -24,7 +24,7 @@ pub async fn instance_info(req: Request<Body>) -> Result<Response<Body>, String>
"yaml" | "yml" => info_yaml(), "yaml" | "yml" => info_yaml(),
"txt" => info_txt(), "txt" => info_txt(),
"json" => info_json(), "json" => info_json(),
"html" | "" => info_html(req), "html" | "" => info_html(&req),
_ => { _ => {
let error = ErrorTemplate { let error = ErrorTemplate {
msg: "Error: Invalid info extension".into(), msg: "Error: Invalid info extension".into(),
@ -68,13 +68,13 @@ fn info_txt() -> Result<Response<Body>, Error> {
Response::builder() Response::builder()
.status(200) .status(200)
.header("content-type", "text/plain") .header("content-type", "text/plain")
.body(Body::from(INSTANCE_INFO.to_string(StringType::Raw))) .body(Body::from(INSTANCE_INFO.to_string(&StringType::Raw)))
} }
fn info_html(req: Request<Body>) -> Result<Response<Body>, Error> { fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
let message = MessageTemplate { let message = MessageTemplate {
title: String::from("Instance information"), title: String::from("Instance information"),
body: INSTANCE_INFO.to_string(StringType::Html), body: INSTANCE_INFO.to_string(&StringType::Html),
prefs: Preferences::new(&req), prefs: Preferences::new(req),
url: req.uri().to_string(), url: req.uri().to_string(),
} }
.render() .render()
@ -109,7 +109,7 @@ impl InstanceInfo {
} }
fn to_table(&self) -> String { fn to_table(&self) -> String {
let mut container = Container::default(); let mut container = Container::default();
let convert = |o: &Option<String>| -> String { o.clone().unwrap_or("<span class=\"unset\"><i>Unset</i></span>".to_owned()) }; let convert = |o: &Option<String>| -> String { o.clone().unwrap_or_else(|| "<span class=\"unset\"><i>Unset</i></span>".to_owned()) };
if let Some(banner) = &self.config.banner { if let Some(banner) = &self.config.banner {
container.add_header(3, "Instance banner"); container.add_header(3, "Instance banner");
container.add_raw("<br />"); container.add_raw("<br />");
@ -136,22 +136,26 @@ impl InstanceInfo {
["Hide awards", &convert(&self.config.default_hide_awards)], ["Hide awards", &convert(&self.config.default_hide_awards)],
["Hide score", &convert(&self.config.default_hide_score)], ["Hide score", &convert(&self.config.default_hide_score)],
["Theme", &convert(&self.config.default_theme)], ["Theme", &convert(&self.config.default_theme)],
["Mascot", &convert(&self.config.default_mascot)],
["Front page", &convert(&self.config.default_front_page)], ["Front page", &convert(&self.config.default_front_page)],
["Layout", &convert(&self.config.default_layout)], ["Layout", &convert(&self.config.default_layout)],
["Wide", &convert(&self.config.default_wide)], ["Wide", &convert(&self.config.default_wide)],
["Comment sort", &convert(&self.config.default_comment_sort)], ["Comment sort", &convert(&self.config.default_comment_sort)],
["Post sort", &convert(&self.config.default_post_sort)], ["Post sort", &convert(&self.config.default_post_sort)],
["Blur Spoiler", &convert(&self.config.default_blur_spoiler)],
["Show NSFW", &convert(&self.config.default_show_nsfw)], ["Show NSFW", &convert(&self.config.default_show_nsfw)],
["Blur NSFW", &convert(&self.config.default_blur_nsfw)], ["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
["Use HLS", &convert(&self.config.default_use_hls)], ["Use HLS", &convert(&self.config.default_use_hls)],
["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)],
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)], ["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)],
]) ])
.with_header_row(["Default preferences"]), .with_header_row(["Default preferences"]),
); );
container.to_html_string().replace("<th>", "<th colspan=\"2\">") container.to_html_string().replace("<th>", "<th colspan=\"2\">")
} }
fn to_string(&self, string_type: StringType) -> String { fn to_string(&self, string_type: &StringType) -> String {
match string_type { match string_type {
StringType::Raw => { StringType::Raw => {
format!( format!(
@ -168,16 +172,20 @@ impl InstanceInfo {
Hide awards: {:?}\n Hide awards: {:?}\n
Hide score: {:?}\n Hide score: {:?}\n
Default theme: {:?}\n Default theme: {:?}\n
Default mascot: {:?}\n
Default front page: {:?}\n Default front page: {:?}\n
Default layout: {:?}\n Default layout: {:?}\n
Default wide: {:?}\n Default wide: {:?}\n
Default comment sort: {:?}\n Default comment sort: {:?}\n
Default post sort: {:?}\n Default post sort: {:?}\n
Default blur Spoiler: {:?}\n
Default show NSFW: {:?}\n Default show NSFW: {:?}\n
Default blur NSFW: {:?}\n Default blur NSFW: {:?}\n
Default use HLS: {:?}\n Default use HLS: {:?}\n
Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n", Default subscriptions: {:?}\n
Default filters: {:?}\n",
self.package_name, self.package_name,
self.crate_version, self.crate_version,
self.git_commit, self.git_commit,
@ -190,16 +198,20 @@ impl InstanceInfo {
self.config.default_hide_awards, self.config.default_hide_awards,
self.config.default_hide_score, self.config.default_hide_score,
self.config.default_theme, self.config.default_theme,
self.config.default_mascot,
self.config.default_front_page, self.config.default_front_page,
self.config.default_layout, self.config.default_layout,
self.config.default_wide, self.config.default_wide,
self.config.default_comment_sort, self.config.default_comment_sort,
self.config.default_post_sort, self.config.default_post_sort,
self.config.default_blur_spoiler,
self.config.default_show_nsfw, self.config.default_show_nsfw,
self.config.default_blur_nsfw, self.config.default_blur_nsfw,
self.config.default_use_hls, self.config.default_use_hls,
self.config.default_ffmpeg_video_downloads,
self.config.default_hide_hls_notification, self.config.default_hide_hls_notification,
self.config.default_subscriptions, self.config.default_subscriptions,
self.config.default_filters,
) )
} }
StringType::Html => self.to_table(), StringType::Html => self.to_table(),

View File

@ -26,7 +26,7 @@ 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 server::RequestExt;
use utils::{error, redirect, ThemeAssets}; use utils::{error, redirect, ThemeAssets, MascotAssets};
use crate::client::OAUTH_CLIENT; use crate::client::OAUTH_CLIENT;
@ -78,6 +78,17 @@ async fn font() -> Result<Response<Body>, String> {
) )
} }
async fn ffmpeg() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/wasm")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(include_bytes!("../static/ffmpeg/ffmpeg-core.wasm").as_ref().into())
.unwrap_or_default(),
)
}
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> { async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
let mut res = Response::builder() let mut res = Response::builder()
.status(200) .status(200)
@ -111,6 +122,20 @@ async fn style() -> Result<Response<Body>, String> {
) )
} }
/// Serve mascot
async fn mascot_image(req: Request<Body>) -> Result<Response<Body>, String> {
let res = MascotAssets::get(&req.param("name").unwrap())
.unwrap_or(MascotAssets::get("redsunlib.png").unwrap());
Ok(
Response::builder()
.status(200)
.header("content-type", "image/png")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(res.data.into())
.unwrap_or_default(),
)
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Load environment variables // Load environment variables
@ -135,7 +160,7 @@ async fn main() {
.long("address") .long("address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Sets address to listen on") .help("Sets address to listen on")
.default_value("0.0.0.0") .default_value("[::]")
.num_args(1), .num_args(1),
) )
.arg( .arg(
@ -166,7 +191,7 @@ async fn main() {
let listener = [address, ":", port].concat(); let listener = [address, ":", port].concat();
println!("Starting Redlib..."); println!("Starting Redsunlib...");
// Begin constructing a server // Begin constructing a server
let mut app = server::Server::new(); let mut app = server::Server::new();
@ -189,11 +214,11 @@ async fn main() {
"Referrer-Policy" => "no-referrer", "Referrer-Policy" => "no-referrer",
"X-Content-Type-Options" => "nosniff", "X-Content-Type-Options" => "nosniff",
"X-Frame-Options" => "DENY", "X-Frame-Options" => "DENY",
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;" "Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src 'self' blob:;"
}; };
if let Some(expire_time) = hsts { if let Some(expire_time) = hsts {
if let Ok(val) = HeaderValue::from_str(&format!("max-age={}", expire_time)) { if let Ok(val) = HeaderValue::from_str(&format!("max-age={expire_time}")) {
app.default_headers.insert("Strict-Transport-Security", val); app.default_headers.insert("Strict-Transport-Security", val);
} }
} }
@ -224,14 +249,34 @@ async fn main() {
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed()); app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed()); app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
app app
.at("/playHLSVideo.js") .at("/videoUtils.js")
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed()); .get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed());
app app
.at("/hls.min.js") .at("/hls.min.js")
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed()); .get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
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());
// FFmpeg
app
.at("/ffmpeg/814.ffmpeg.js")
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js"), "text/javascript", false).boxed());
app
.at("/ffmpeg/814.ffmpeg.js.map")
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js.map"), "text/javascript", false).boxed());
app
.at("/ffmpeg/ffmpeg-core.js")
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-core.js"), "text/javascript", false).boxed());
app.at("/ffmpeg/ffmpeg-core.wasm").get(|_| ffmpeg().boxed());
app
.at("/ffmpeg/ffmpeg-util.js")
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-util.js"), "text/javascript", false).boxed());
app
.at("/ffmpeg/ffmpeg.js")
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js"), "text/javascript", false).boxed());
app
.at("/ffmpeg/ffmpeg.js.map")
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js.map"), "text/javascript", false).boxed());
// Proxy media through Redlib // Proxy media through Redlib
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed()); app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
@ -249,11 +294,11 @@ async fn main() {
// Browse user profile // Browse user profile
app app
.at("/u/:name") .at("/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("/u/:name/comments/:id/:title").get(|r| post::item(r).boxed()); app.at("/u/:name/comments/:id/:title").get(|r| post::item(r).boxed());
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed()); app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed()); app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed()); app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed()); app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed()); app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -265,6 +310,9 @@ async fn main() {
app.at("/settings/restore").get(|r| settings::restore(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed()); app.at("/settings/update").get(|r| settings::update(r).boxed());
// Mascots
app.at("/mascot/:name").get(|r| mascot_image(r).boxed());
// Subreddit services // Subreddit services
app app
.at("/r/:sub") .at("/r/:sub")
@ -273,7 +321,7 @@ async fn main() {
app app
.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(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed()); app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
@ -298,10 +346,10 @@ async fn main() {
app app
.at("/r/:sub/w") .at("/r/:sub/w")
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed()); .get(|r| async move { Ok(redirect(&format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed());
app app
.at("/r/:sub/w/*page") .at("/r/:sub/w/*page")
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki/{}", r.param("sub").unwrap_or_default(), r.param("wiki").unwrap_or_default()))) }.boxed()); .get(|r| async move { Ok(redirect(&format!("/r/{}/wiki/{}", r.param("sub").unwrap_or_default(), r.param("wiki").unwrap_or_default()))) }.boxed());
app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed()); app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed());
app.at("/r/:sub/wiki/*page").get(|r| subreddit::wiki(r).boxed()); app.at("/r/:sub/wiki/*page").get(|r| subreddit::wiki(r).boxed());
@ -313,10 +361,10 @@ async fn main() {
app.at("/").get(|r| subreddit::community(r).boxed()); app.at("/").get(|r| subreddit::community(r).boxed());
// View Reddit wiki // View Reddit wiki
app.at("/w").get(|_| async { Ok(redirect("/wiki".to_string())) }.boxed()); app.at("/w").get(|_| async { Ok(redirect("/wiki")) }.boxed());
app app
.at("/w/*page") .at("/w/*page")
.get(|r| async move { Ok(redirect(format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed()); .get(|r| async move { Ok(redirect(&format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed());
app.at("/wiki").get(|r| subreddit::wiki(r).boxed()); app.at("/wiki").get(|r| subreddit::wiki(r).boxed());
app.at("/wiki/*page").get(|r| subreddit::wiki(r).boxed()); app.at("/wiki/*page").get(|r| subreddit::wiki(r).boxed());
@ -324,7 +372,7 @@ async fn main() {
app.at("/search").get(|r| search::find(r).boxed()); app.at("/search").get(|r| search::find(r).boxed());
// Handle about pages // Handle about pages
app.at("/about").get(|req| error(req, "About pages aren't added yet".to_string()).boxed()); app.at("/about").get(|req| error(req, "About pages aren't added yet").boxed());
// Instance info page // Instance info page
app.at("/info").get(|r| instance_info::instance_info(r).boxed()); app.at("/info").get(|r| instance_info::instance_info(r).boxed());
@ -337,14 +385,14 @@ 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/{}/s/{}", sub, id)).await { Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).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,
}, },
// Error message for unknown pages // Error message for unknown pages
_ => error(req, "Nothing here".to_string()).await, _ => error(req, "Nothing here").await,
} }
}) })
}); });
@ -356,29 +404,29 @@ 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}")).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,
}, },
Err(e) => error(req, e).await, Err(e) => error(req, &e).await,
}, },
// Error message for unknown pages // Error message for unknown pages
_ => error(req, "Nothing here".to_string()).await, _ => error(req, "Nothing here").await,
} }
}) })
}); });
// Default service in case no routes match // Default service in case no routes match
app.at("/*").get(|req| error(req, "Nothing here".to_string()).boxed()); app.at("/*").get(|req| error(req, "Nothing here").boxed());
println!("Running Redlib v{} on {}!", env!("CARGO_PKG_VERSION"), listener); println!("Running Redsunlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
let server = app.listen(listener); let server = app.listen(&listener);
// Run this server for... forever! // Run this server for... forever!
if let Err(e) = server.await { if let Err(e) = server.await {
eprintln!("Server error: {}", e); eprintln!("Server error: {e}");
} }
} }

View File

@ -1,12 +1,12 @@
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
use crate::{ use crate::{
client::{CLIENT, OAUTH_CLIENT}, client::{CLIENT, OAUTH_CLIENT, OAUTH_RATELIMIT_REMAINING},
oauth_resources::ANDROID_APP_VERSION_LIST, oauth_resources::ANDROID_APP_VERSION_LIST,
}; };
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use hyper::{client, Body, Method, Request}; use hyper::{client, Body, Method, Request};
use log::info; use log::{info, trace};
use serde_json::json; use serde_json::json;
@ -46,11 +46,11 @@ 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!("{}/api/access_token", AUTH_ENDPOINT); let url = format!("{AUTH_ENDPOINT}/api/access_token");
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
for (key, value) in self.initial_headers.iter() { for (key, value) in &self.initial_headers {
builder = builder.header(key, value); builder = builder.header(key, value);
} }
// Set up HTTP Basic Auth - basically just the const OAuth ID's with no password, // Set up HTTP Basic Auth - basically just the const OAuth ID's with no password,
@ -70,7 +70,7 @@ impl Oauth {
let request = builder.body(body).unwrap(); let request = builder.body(body).unwrap();
// Send request // Send request
let client: client::Client<_, hyper::Body> = CLIENT.clone(); let client: client::Client<_, Body> = CLIENT.clone();
let resp = client.request(request).await.ok()?; let resp = client.request(request).await.ok()?;
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes. // Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
@ -129,6 +129,12 @@ pub async fn token_daemon() {
} }
} }
} }
pub async fn force_refresh_token() {
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
OAUTH_CLIENT.write().await.refresh().await;
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct Device { struct Device {
oauth_id: String, oauth_id: String,

View File

@ -2,76 +2,43 @@
// Rerun scripts/update_oauth_resources.sh to update this file // Rerun scripts/update_oauth_resources.sh to update this file
// Please do not edit manually // Please do not edit manually
// Filled in with real app versions // Filled in with real app versions
pub static _IOS_APP_VERSION_LIST: &[&str; 67] = &[ pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
"Version 2020.0.0/Build 306960",
"Version 2020.10.0/Build 307041",
"Version 2020.10.1/Build 307047",
"Version 2020.1.0/Build 306966",
"Version 2020.11.0/Build 307049",
"Version 2020.11.1/Build 307063",
"Version 2020.12.0/Build 307070",
"Version 2020.13.0/Build 307072",
"Version 2020.13.1/Build 307075",
"Version 2020.14.0/Build 307077",
"Version 2020.14.1/Build 307080",
"Version 2020.15.0/Build 307084",
"Version 2020.16.0/Build 307090",
"Version 2020.17.0/Build 307093",
"Version 2020.19.0/Build 307137",
"Version 2020.20.0/Build 307156",
"Version 2020.20.1/Build 307159",
"Version 2020.2.0/Build 306969",
"Version 2020.21.0/Build 307162",
"Version 2020.21.1/Build 307165",
"Version 2020.22.0/Build 307177",
"Version 2020.22.1/Build 307181",
"Version 2020.23.0/Build 307183",
"Version 2020.24.0/Build 307189",
"Version 2020.25.0/Build 307198",
"Version 2020.26.0/Build 307205",
"Version 2020.26.1/Build 307213",
"Version 2020.27.0/Build 307229",
"Version 2020.28.0/Build 307233",
"Version 2020.29.0/Build 307235",
"Version 2020.30.0/Build 307238",
"Version 2020.3.0/Build 306971",
"Version 2020.31.0/Build 307240",
"Version 2020.31.1/Build 307246",
"Version 2020.32.0/Build 307250",
"Version 2020.33.0/Build 307252",
"Version 2020.34.0/Build 307260",
"Version 2020.35.0/Build 307262",
"Version 2020.36.0/Build 307265",
"Version 2020.37.0/Build 307272",
"Version 2020.38.0/Build 307286",
"Version 2020.39.0/Build 307306",
"Version 2020.4.0/Build 306978",
"Version 2020.5.0/Build 306993",
"Version 2020.5.1/Build 307005",
"Version 2020.6.0/Build 307007",
"Version 2020.7.0/Build 307012",
"Version 2020.8.0/Build 307014",
"Version 2020.8.1/Build 307017",
"Version 2020.9.0/Build 307035",
"Version 2020.9.1/Build 307039",
"Version 2023.18.0/Build 310494",
"Version 2023.19.0/Build 310507",
"Version 2023.20.0/Build 310535",
"Version 2023.21.0/Build 310560",
"Version 2023.22.0/Build 613580",
"Version 2023.23.0/Build 310613",
"Version 2023.23.1/Build 613639",
"Version 2023.24.0/Build 613663",
"Version 2023.25.0/Build 613739",
"Version 2023.26.0/Build 613749",
"Version 2023.27.0/Build 613771",
"Version 2023.28.0/Build 613803",
"Version 2023.28.1/Build 613809",
"Version 2023.29.0/Build 613825",
"Version 2023.30.0/Build 613849",
"Version 2023.31.0/Build 613864",
];
pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.48.0/Build 1319123",
"Version 2023.49.0/Build 1321715",
"Version 2023.49.1/Build 1322281",
"Version 2023.50.0/Build 1332338",
"Version 2023.50.1/Build 1345844",
"Version 2024.02.0/Build 1368985",
"Version 2024.03.0/Build 1379408",
"Version 2024.04.0/Build 1391236",
"Version 2024.05.0/Build 1403584",
"Version 2024.06.0/Build 1418489",
"Version 2024.07.0/Build 1429651",
"Version 2024.08.0/Build 1439531",
"Version 2024.10.0/Build 1470045",
"Version 2024.10.1/Build 1478645",
"Version 2024.11.0/Build 1480707",
"Version 2024.12.0/Build 1494694",
"Version 2024.13.0/Build 1505187",
"Version 2024.14.0/Build 1520556",
"Version 2024.15.0/Build 1536823",
"Version 2024.16.0/Build 1551366",
"Version 2024.17.0/Build 1568106",
"Version 2024.18.0/Build 1577901",
"Version 2024.18.1/Build 1585304",
"Version 2024.19.0/Build 1593346",
"Version 2024.20.0/Build 1612800",
"Version 2024.20.1/Build 1615586",
"Version 2024.20.2/Build 1624969",
"Version 2024.21.0/Build 1631686",
"Version 2024.22.0/Build 1645257",
"Version 2024.22.1/Build 1652272",
"Version 2023.21.0/Build 956283",
"Version 2023.22.0/Build 968223",
"Version 2023.23.0/Build 983896",
"Version 2023.24.0/Build 998541",
"Version 2023.25.0/Build 1014750",
"Version 2023.25.1/Build 1018737", "Version 2023.25.1/Build 1018737",
"Version 2023.26.0/Build 1019073", "Version 2023.26.0/Build 1019073",
"Version 2023.27.0/Build 1031923", "Version 2023.27.0/Build 1031923",
@ -97,11 +64,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.44.0/Build 1268622", "Version 2023.44.0/Build 1268622",
"Version 2023.45.0/Build 1281371", "Version 2023.45.0/Build 1281371",
"Version 2023.47.0/Build 1303604", "Version 2023.47.0/Build 1303604",
"Version 2023.48.0/Build 1319123", "Version 2022.42.0/Build 638508",
"Version 2023.49.0/Build 1321715", "Version 2022.43.0/Build 648277",
"Version 2023.49.1/Build 1322281", "Version 2022.44.0/Build 664348",
"Version 2023.50.0/Build 1332338", "Version 2022.45.0/Build 677985",
"Version 2023.50.1/Build 1345844", "Version 2023.01.0/Build 709875",
"Version 2023.02.0/Build 717912", "Version 2023.02.0/Build 717912",
"Version 2023.03.0/Build 729220", "Version 2023.03.0/Build 729220",
"Version 2023.04.0/Build 744681", "Version 2023.04.0/Build 744681",
@ -127,11 +94,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.19.0/Build 927681", "Version 2023.19.0/Build 927681",
"Version 2023.20.0/Build 943980", "Version 2023.20.0/Build 943980",
"Version 2023.20.1/Build 946732", "Version 2023.20.1/Build 946732",
"Version 2023.21.0/Build 956283", "Version 2022.20.0/Build 487703",
"Version 2023.22.0/Build 968223", "Version 2022.21.0/Build 492436",
"Version 2023.23.0/Build 983896", "Version 2022.22.0/Build 498700",
"Version 2023.24.0/Build 998541", "Version 2022.23.0/Build 502374",
"Version 2023.25.0/Build 1014750", "Version 2022.23.1/Build 506606",
"Version 2022.24.0/Build 510950", "Version 2022.24.0/Build 510950",
"Version 2022.24.1/Build 513462", "Version 2022.24.1/Build 513462",
"Version 2022.25.0/Build 515072", "Version 2022.25.0/Build 515072",
@ -157,11 +124,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.40.0/Build 624782", "Version 2022.40.0/Build 624782",
"Version 2022.41.0/Build 630468", "Version 2022.41.0/Build 630468",
"Version 2022.41.1/Build 634168", "Version 2022.41.1/Build 634168",
"Version 2022.42.0/Build 638508", "Version 2021.39.1/Build 372418",
"Version 2022.43.0/Build 648277", "Version 2021.41.0/Build 376052",
"Version 2022.44.0/Build 664348", "Version 2021.42.0/Build 378193",
"Version 2022.45.0/Build 677985", "Version 2021.43.0/Build 382019",
"Version 2023.01.0/Build 709875", "Version 2021.44.0/Build 385129",
"Version 2021.45.0/Build 387663", "Version 2021.45.0/Build 387663",
"Version 2021.46.0/Build 392043", "Version 2021.46.0/Build 392043",
"Version 2021.47.0/Build 394342", "Version 2021.47.0/Build 394342",
@ -177,12 +144,7 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.17.0/Build 468480", "Version 2022.17.0/Build 468480",
"Version 2022.18.0/Build 473740", "Version 2022.18.0/Build 473740",
"Version 2022.19.1/Build 482464", "Version 2022.19.1/Build 482464",
"Version 2022.20.0/Build 487703",
"Version 2022.2.0/Build 405543", "Version 2022.2.0/Build 405543",
"Version 2022.21.0/Build 492436",
"Version 2022.22.0/Build 498700",
"Version 2022.23.0/Build 502374",
"Version 2022.23.1/Build 506606",
"Version 2022.3.0/Build 408637", "Version 2022.3.0/Build 408637",
"Version 2022.4.0/Build 411368", "Version 2022.4.0/Build 411368",
"Version 2022.5.0/Build 414731", "Version 2022.5.0/Build 414731",
@ -192,44 +154,5 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.7.0/Build 420849", "Version 2022.7.0/Build 420849",
"Version 2022.8.0/Build 423906", "Version 2022.8.0/Build 423906",
"Version 2022.9.0/Build 426592", "Version 2022.9.0/Build 426592",
"Version 2021.17.0/Build 323213",
"Version 2021.18.0/Build 324849",
"Version 2021.19.0/Build 325762",
"Version 2021.20.0/Build 326964",
"Version 2021.21.0/Build 327703",
"Version 2021.21.1/Build 328461",
"Version 2021.22.0/Build 329696",
"Version 2021.23.0/Build 331631",
"Version 2021.24.0/Build 333951",
"Version 2021.25.0/Build 335451",
"Version 2021.26.0/Build 336739",
"Version 2021.27.0/Build 338857",
"Version 2021.28.0/Build 340747",
"Version 2021.29.0/Build 342342",
"Version 2021.30.0/Build 343820",
"Version 2021.31.0/Build 346485",
"Version 2021.32.0/Build 349507",
"Version 2021.33.0/Build 351843",
"Version 2021.34.0/Build 353911",
"Version 2021.35.0/Build 355878",
"Version 2021.36.0/Build 359254",
"Version 2021.36.1/Build 360572",
"Version 2021.37.0/Build 361905",
"Version 2021.38.0/Build 365032",
"Version 2021.39.0/Build 369068",
"Version 2021.39.1/Build 372418",
"Version 2021.41.0/Build 376052",
"Version 2021.42.0/Build 378193",
"Version 2021.43.0/Build 382019",
"Version 2021.44.0/Build 385129",
];
pub static _IOS_OS_VERSION_LIST: &[&str; 8] = &[
"Version 17.0.1 (Build 21A340)",
"Version 17.0.2 (Build 21A350)",
"Version 17.0.3 (Build 21A360)",
"Version 17.1 (Build 21B74)",
"Version 17.1.1 (Build 21B91)",
"Version 17.1.2 (Build 21B101)",
"Version 17.2 (Build 21C62)",
"Version 17.2.1 (Build 21C66)",
]; ];
pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""];

View File

@ -27,7 +27,7 @@ struct PostTemplate {
comment_query: String, comment_query: String,
} }
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap()); static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\?q=(.*)&type=comment").unwrap());
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path // Build Reddit API path
@ -52,7 +52,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Log the post ID being fetched in debug mode // Log the post ID being fetched in debug mode
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
dbg!(req.param("id").unwrap_or_default()); req.param("id").unwrap_or_default();
let single_thread = req.param("comment_id").is_some(); let single_thread = req.param("comment_id").is_some();
let highlighted_comment = &req.param("comment_id").unwrap_or_default(); let highlighted_comment = &req.param("comment_id").unwrap_or_default();
@ -83,7 +83,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
}; };
// Use the Post and Comment structs to generate a website to show users // Use the Post and Comment structs to generate a website to show users
template(PostTemplate { Ok(template(&PostTemplate {
comments, comments,
post, post,
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(), url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
@ -92,15 +92,15 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
single_thread, single_thread,
url: req_url, url: req_url,
comment_query: query, comment_query: query,
}) }))
} }
// If the Reddit API returns an error, exit and send error page to user // If the Reddit API returns an error, exit and send error page to user
Err(msg) => { Err(msg) => {
if msg == "quarantined" || msg == "gated" { if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub, msg) Ok(quarantine(&req, sub, &msg))
} else { } else {
error(req, msg).await error(req, &msg).await
} }
} }
} }
@ -139,19 +139,19 @@ fn query_comments(
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned); let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
let mut results = Vec::new(); let mut results = Vec::new();
comments.into_iter().for_each(|comment| { for comment in comments {
let data = &comment["data"]; let data = &comment["data"];
// If this comment contains replies, handle those too // If this comment contains replies, handle those too
if data["replies"].is_object() { if data["replies"].is_object() {
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req)) results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req));
} }
let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req); let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
if c.body.to_lowercase().contains(&query.to_lowercase()) { if c.body.to_lowercase().contains(&query.to_lowercase()) {
results.push(c); results.push(c);
} }
}); }
results results
} }
@ -170,10 +170,8 @@ fn build_comment(
let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" { let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" {
format!( format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}{}\">view removed comment</a></p></div>", "<div class=\"md\"><p>[removed] — <a href=\"https://{}{post_link}{id}\">view removed comment</a></p></div>",
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)), get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
post_link,
id
) )
} else { } else {
rewrite_urls(&val(comment, "body_html")) rewrite_urls(&val(comment, "body_html"))

View File

@ -65,11 +65,11 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
query = REDDIT_URL_MATCH.replace(&query, "").to_string(); query = REDDIT_URL_MATCH.replace(&query, "").to_string();
if query.is_empty() { if query.is_empty() {
return Ok(redirect("/".to_string())); return Ok(redirect("/"));
} }
if query.starts_with("r/") { if query.starts_with("r/") {
return Ok(redirect(format!("/{}", query))); return Ok(redirect(&format!("/{query}")));
} }
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
@ -97,7 +97,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
// If all requested subs are filtered, we don't need to fetch posts. // If all requested subs are filtered, we don't need to fetch posts.
if sub.split('+').all(|s| filters.contains(s)) { if sub.split('+').all(|s| filters.contains(s)) {
template(SearchTemplate { Ok(template(&SearchTemplate {
posts: Vec::new(), posts: Vec::new(),
subreddits, subreddits,
sub, sub,
@ -106,7 +106,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
sort, sort,
t: param(&path, "t").unwrap_or_default(), t: param(&path, "t").unwrap_or_default(),
before: param(&path, "after").unwrap_or_default(), before: param(&path, "after").unwrap_or_default(),
after: "".to_string(), after: String::new(),
restrict_sr: param(&path, "restrict_sr").unwrap_or_default(), restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
typed, typed,
}, },
@ -116,14 +116,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false, no_posts: false,
}) }))
} else { } else {
match Post::fetch(&path, quarantined).await { match Post::fetch(&path, quarantined).await {
Ok((mut posts, after)) => { Ok((mut posts, after)) => {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty(); let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(SearchTemplate { Ok(template(&SearchTemplate {
posts, posts,
subreddits, subreddits,
sub, sub,
@ -142,14 +142,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts, no_posts,
}) }))
} }
Err(msg) => { Err(msg) => {
if msg == "quarantined" || msg == "gated" { if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub, msg) Ok(quarantine(&req, sub, &msg))
} else { } else {
error(req, msg).await error(req, &msg).await
} }
} }
} }
@ -158,7 +158,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> { async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
let limit = if typed == "sr_user" { "50" } else { "3" }; let limit = if typed == "sr_user" { "50" } else { "3" };
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit={}", q.replace(' ', "+"), limit); let subreddit_search_path = format!("/subreddits/search.json?q={}&limit={limit}", q.replace(' ', "+"));
// Send a request to the url // Send a request to the url
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"] json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
use brotli::enc::{BrotliCompress, BrotliEncoderParams}; use brotli::enc::{BrotliCompress, BrotliEncoderParams};
use cached::proc_macro::cached; use cached::proc_macro::cached;
use cookie::Cookie; use cookie::Cookie;
@ -15,6 +17,7 @@ use libflate::gzip;
use route_recognizer::{Params, Router}; use route_recognizer::{Params, Router};
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
fmt::Display,
io, io,
pin::Pin, pin::Pin,
result::Result, result::Result,
@ -65,12 +68,12 @@ impl CompressionType {
} }
} }
impl ToString for CompressionType { impl Display for CompressionType {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Gzip => "gzip".to_string(), Self::Gzip => write!(f, "gzip"),
Self::Brotli => "br".to_string(), Self::Brotli => write!(f, "br"),
_ => String::new(), Self::Passthrough => Ok(()),
} }
} }
} }
@ -104,13 +107,13 @@ pub trait RequestExt {
fn params(&self) -> Params; fn params(&self) -> Params;
fn param(&self, name: &str) -> Option<String>; fn param(&self, name: &str) -> Option<String>;
fn set_params(&mut self, params: Params) -> Option<Params>; fn set_params(&mut self, params: Params) -> Option<Params>;
fn cookies(&self) -> Vec<Cookie>; fn cookies(&self) -> Vec<Cookie<'_>>;
fn cookie(&self, name: &str) -> Option<Cookie>; fn cookie(&self, name: &str) -> Option<Cookie<'_>>;
} }
pub trait ResponseExt { pub trait ResponseExt {
fn cookies(&self) -> Vec<Cookie>; fn cookies(&self) -> Vec<Cookie<'_>>;
fn insert_cookie(&mut self, cookie: Cookie); fn insert_cookie(&mut self, cookie: Cookie<'_>);
fn remove_cookie(&mut self, name: String); fn remove_cookie(&mut self, name: String);
} }
@ -131,7 +134,7 @@ impl RequestExt for Request<Body> {
self.extensions_mut().insert(params) self.extensions_mut().insert(params)
} }
fn cookies(&self) -> Vec<Cookie> { fn cookies(&self) -> Vec<Cookie<'_>> {
self.headers().get("Cookie").map_or(Vec::new(), |header| { self.headers().get("Cookie").map_or(Vec::new(), |header| {
header header
.to_str() .to_str()
@ -142,13 +145,13 @@ impl RequestExt for Request<Body> {
}) })
} }
fn cookie(&self, name: &str) -> Option<Cookie> { fn cookie(&self, name: &str) -> Option<Cookie<'_>> {
self.cookies().into_iter().find(|c| c.name() == name) self.cookies().into_iter().find(|c| c.name() == name)
} }
} }
impl ResponseExt for Response<Body> { impl ResponseExt for Response<Body> {
fn cookies(&self) -> Vec<Cookie> { fn cookies(&self) -> Vec<Cookie<'_>> {
self.headers().get("Cookie").map_or(Vec::new(), |header| { self.headers().get("Cookie").map_or(Vec::new(), |header| {
header header
.to_str() .to_str()
@ -159,7 +162,7 @@ impl ResponseExt for Response<Body> {
}) })
} }
fn insert_cookie(&mut self, cookie: Cookie) { fn insert_cookie(&mut self, cookie: Cookie<'_>) {
if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) { if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) {
self.headers_mut().append("Set-Cookie", val); self.headers_mut().append("Set-Cookie", val);
} }
@ -176,19 +179,19 @@ impl ResponseExt for Response<Body> {
} }
impl Route<'_> { impl Route<'_> {
fn method(&mut self, method: Method, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self { fn method(&mut self, method: &Method, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
self.router.add(&format!("/{}{}", method.as_str(), self.path), dest); self.router.add(&format!("/{}{}", method.as_str(), self.path), dest);
self self
} }
/// Add an endpoint for `GET` requests /// Add an endpoint for `GET` requests
pub fn get(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self { pub fn get(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
self.method(Method::GET, dest) self.method(&Method::GET, dest)
} }
/// Add an endpoint for `POST` requests /// Add an endpoint for `POST` requests
pub fn post(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self { pub fn post(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
self.method(Method::POST, dest) self.method(&Method::POST, dest)
} }
} }
@ -200,14 +203,14 @@ impl Server {
} }
} }
pub fn at(&mut self, path: &str) -> Route { pub fn at(&mut self, path: &str) -> Route<'_> {
Route { Route {
path: path.to_owned(), path: path.to_owned(),
router: &mut self.router, router: &mut self.router,
} }
} }
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> { pub fn listen(self, addr: &str) -> Boxed<Result<(), hyper::Error>> {
let make_svc = make_service_fn(move |_conn| { let make_svc = make_service_fn(move |_conn| {
// For correct borrowing, these values need to be borrowed // For correct borrowing, these values need to be borrowed
let router = self.router.clone(); let router = self.router.clone();
@ -260,7 +263,7 @@ impl Server {
}); });
// Build SocketAddr from provided address // Build SocketAddr from provided address
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr)); let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {addr} as address (example format: 0.0.0.0:8080)"));
// Bind server to address specified above. Gracefully shut down if CTRL+C is pressed // Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async { let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
@ -376,7 +379,7 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
// The compressor and q-value (if the latter is defined) // The compressor and q-value (if the latter is defined)
// will be delimited by semicolons. // will be delimited by semicolons.
let mut spl: Split<char> = val.split(';'); let mut spl: Split<'_, char> = val.split(';');
// Get the compressor. For example, in // Get the compressor. For example, in
// gzip;q=0.8 // gzip;q=0.8
@ -438,10 +441,10 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
}; };
} }
if cur_candidate.q != f64::NEG_INFINITY { if cur_candidate.q == f64::NEG_INFINITY {
Some(cur_candidate.alg)
} else {
None None
} else {
Some(cur_candidate.alg)
} }
} }
@ -453,16 +456,16 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
/// conditions are met: /// conditions are met:
/// ///
/// 1. the HTTP client requests a compression encoding in the Content-Encoding /// 1. the HTTP client requests a compression encoding in the Content-Encoding
/// header (hence the need for the req_headers); /// header (hence the need for the `req_headers`);
/// ///
/// 2. the content encoding corresponds to a compression algorithm we support; /// 2. the content encoding corresponds to a compression algorithm we support;
/// ///
/// 3. the Media type in the Content-Type response header is text with any /// 3. the Media type in the Content-Type response header is text with any
/// subtype (e.g. text/plain) or application/json. /// subtype (e.g. text/plain) or application/json.
/// ///
/// compress_response returns Ok on successful compression, or if not all three /// `compress_response` returns Ok on successful compression, or if not all three
/// conditions above are met. It returns Err if there was a problem decoding /// conditions above are met. It returns Err if there was a problem decoding
/// any header in either req_headers or res, but res will remain intact. /// any header in either `req_headers` or res, but res will remain intact.
/// ///
/// This function logs errors to stderr, but only in debug mode. No information /// This function logs errors to stderr, but only in debug mode. No information
/// is logged in release builds. /// is logged in release builds.
@ -601,7 +604,7 @@ fn compress_body(compressor: CompressionType, body_bytes: Vec<u8>) -> Result<Vec
// This arm is for any requested compressor for which we don't yet // This arm is for any requested compressor for which we don't yet
// have an implementation. // have an implementation.
_ => { CompressionType::Passthrough => {
let msg = "unsupported compressor".to_string(); let msg = "unsupported compressor".to_string();
return Err(msg); return Err(msg);
} }
@ -677,7 +680,7 @@ mod tests {
// Perform the compression. // Perform the compression.
if let Err(e) = block_on(compress_response(&req_headers, &mut res)) { if let Err(e) = block_on(compress_response(&req_headers, &mut res)) {
panic!("compress_response(&req_headers, &mut res) => Err(\"{}\")", e); panic!("compress_response(&req_headers, &mut res) => Err(\"{e}\")");
}; };
// If the content was compressed, we expect the Content-Encoding // If the content was compressed, we expect the Content-Encoding
@ -699,7 +702,7 @@ mod tests {
// the Response is the same as what with which we start. // the Response is the same as what with which we start.
let body_vec = match block_on(body::to_bytes(res.body_mut())) { let body_vec = match block_on(body::to_bytes(res.body_mut())) {
Ok(b) => b.to_vec(), Ok(b) => b.to_vec(),
Err(e) => panic!("{}", e), Err(e) => panic!("{e}"),
}; };
if expected_encoding == CompressionType::Passthrough { if expected_encoding == CompressionType::Passthrough {
@ -715,7 +718,7 @@ mod tests {
let mut decoder: Box<dyn io::Read> = match expected_encoding { let mut decoder: Box<dyn io::Read> = match expected_encoding {
CompressionType::Gzip => match gzip::Decoder::new(&mut body_cursor) { CompressionType::Gzip => match gzip::Decoder::new(&mut body_cursor) {
Ok(dgz) => Box::new(dgz), Ok(dgz) => Box::new(dgz),
Err(e) => panic!("{}", e), Err(e) => panic!("{e}"),
}, },
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())), CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
@ -725,7 +728,7 @@ mod tests {
let mut decompressed = Vec::<u8>::new(); let mut decompressed = Vec::<u8>::new();
if let Err(e) = io::copy(&mut decoder, &mut decompressed) { if let Err(e) = io::copy(&mut decoder, &mut decompressed) {
panic!("{}", e); panic!("{e}");
}; };
assert!(decompressed.eq(&expected_lorem_ipsum)); assert!(decompressed.eq(&expected_lorem_ipsum));

View File

@ -19,18 +19,22 @@ struct SettingsTemplate {
// CONSTANTS // CONSTANTS
const PREFS: [&str; 15] = [ const PREFS: [&str; 19] = [
"theme", "theme",
"mascot",
"front_page", "front_page",
"layout", "layout",
"wide", "wide",
"comment_sort", "comment_sort",
"post_sort", "post_sort",
"blur_spoiler",
"show_nsfw", "show_nsfw",
"blur_nsfw", "blur_nsfw",
"use_hls", "use_hls",
"ffmpeg_video_downloads",
"hide_hls_notification", "hide_hls_notification",
"autoplay_videos", "autoplay_videos",
"hide_sidebar_and_summary",
"fixed_navbar", "fixed_navbar",
"hide_awards", "hide_awards",
"hide_score", "hide_score",
@ -42,10 +46,10 @@ const PREFS: [&str; 15] = [
// Retrieve cookies from request "Cookie" header // Retrieve cookies from request "Cookie" header
pub async fn get(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn get(req: Request<Body>) -> Result<Response<Body>, String> {
let url = req.uri().to_string(); let url = req.uri().to_string();
template(SettingsTemplate { Ok(template(&SettingsTemplate {
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url, url,
}) }))
} }
// Set cookies using response "Set-Cookie" header // Set cookies using response "Set-Cookie" header
@ -54,7 +58,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let (parts, mut body) = req.into_parts(); let (parts, mut body) = req.into_parts();
// Grab existing cookies // Grab existing cookies
let _cookies: Vec<Cookie> = parts let _cookies: Vec<Cookie<'_>> = parts
.headers .headers
.get_all("Cookie") .get_all("Cookie")
.iter() .iter()
@ -73,14 +77,14 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>(); let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
let mut response = redirect("/settings".to_string()); let mut response = redirect("/settings");
for &name in &PREFS { for &name in &PREFS {
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()))
.path("/") .path("/")
.http_only(true) .http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52)) .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(), .into(),
), ),
@ -96,7 +100,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
let (parts, _) = req.into_parts(); let (parts, _) = req.into_parts();
// Grab existing cookies // Grab existing cookies
let _cookies: Vec<Cookie> = parts let _cookies: Vec<Cookie<'_>> = parts
.headers .headers
.get_all("Cookie") .get_all("Cookie")
.iter() .iter()
@ -112,14 +116,14 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
None => "/".to_string(), None => "/".to_string(),
}; };
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"]].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()))
.path("/") .path("/")
.http_only(true) .http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52)) .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(), .into(),
), ),

View File

@ -7,6 +7,8 @@ use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy;
use regex::Regex;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
// STRUCTS // STRUCTS
@ -50,16 +52,19 @@ struct WallTemplate {
url: String, url: String,
} }
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
// SERVICES // SERVICES
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path // Build Reddit API path
let root = req.uri().path() == "/"; let root = req.uri().path() == "/";
let query = req.uri().query().unwrap_or_default().to_string();
let subscribed = setting(&req, "subscriptions"); let subscribed = setting(&req, "subscriptions");
let front_page = setting(&req, "front_page"); let front_page = setting(&req, "front_page");
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort)); let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
let sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() { let mut sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
if subscribed.is_empty() { if subscribed.is_empty() {
"popular".to_string() "popular".to_string()
} else { } else {
@ -76,7 +81,12 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
} }
if req.param("sub").is_some() && sub_name.starts_with("u_") { if req.param("sub").is_some() && sub_name.starts_with("u_") {
return Ok(redirect(["/user/", &sub_name[2..]].concat())); return Ok(redirect(&["/user/", &sub_name[2..]].concat()));
}
// If multi-sub, replace + with url encoded +
if sub_name.contains('+') {
sub_name = sub_name.replace('+', "%2B");
} }
// Request subreddit metadata // Request subreddit metadata
@ -107,7 +117,11 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let mut params = String::from("&raw_json=1"); let mut params = String::from("&raw_json=1");
if sub_name == "popular" { if sub_name == "popular" {
params.push_str("&geo_filter=GLOBAL"); let geo_filter = match GEO_FILTER_MATCH.captures(&query) {
Some(geo_filter) => geo_filter["region"].to_string(),
None => "GLOBAL".to_owned(),
};
params.push_str(&format!("&geo_filter={geo_filter}"));
} }
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default()); let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default());
@ -117,11 +131,11 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
// If all requested subs are filtered, we don't need to fetch posts. // If all requested subs are filtered, we don't need to fetch posts.
if sub_name.split('+').all(|s| filters.contains(s)) { if sub_name.split('+').all(|s| filters.contains(s)) {
template(SubredditTemplate { Ok(template(&SubredditTemplate {
sub, sub,
posts: Vec::new(), posts: Vec::new(),
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after").unwrap_or_default(), "".to_string()), ends: (param(&path, "after").unwrap_or_default(), String::new()),
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url, url,
redirect_url, redirect_url,
@ -129,14 +143,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false, no_posts: false,
}) }))
} else { } else {
match Post::fetch(&path, quarantined).await { match Post::fetch(&path, quarantined).await {
Ok((mut posts, after)) => { Ok((mut posts, after)) => {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty(); let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(SubredditTemplate { Ok(template(&SubredditTemplate {
sub, sub,
posts, posts,
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
@ -148,40 +162,38 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts, no_posts,
}) }))
} }
Err(msg) => match msg.as_str() { Err(msg) => match msg.as_str() {
"quarantined" | "gated" => quarantine(req, sub_name, msg), "quarantined" | "gated" => Ok(quarantine(&req, sub_name, &msg)),
"private" => error(req, format!("r/{} is a private community", sub_name)).await, "private" => error(req, &format!("r/{sub_name} is a private community")).await,
"banned" => error(req, format!("r/{} has been banned from Reddit", sub_name)).await, "banned" => error(req, &format!("r/{sub_name} has been banned from Reddit")).await,
_ => error(req, msg).await, _ => error(req, &msg).await,
}, },
} }
} }
} }
pub fn quarantine(req: Request<Body>, sub: String, restriction: String) -> Result<Response<Body>, String> { pub fn quarantine(req: &Request<Body>, sub: String, restriction: &str) -> Response<Body> {
let wall = WallTemplate { let wall = WallTemplate {
title: format!("r/{} is {}", sub, restriction), title: format!("r/{sub} is {restriction}"),
msg: "Please click the button below to continue to this subreddit.".to_string(), msg: "Please click the button below to continue to this subreddit.".to_string(),
url: req.uri().to_string(), url: req.uri().to_string(),
sub, sub,
prefs: Preferences::new(&req), prefs: Preferences::new(req),
}; };
Ok( Response::builder()
Response::builder() .status(403)
.status(403) .header("content-type", "text/html")
.header("content-type", "text/html") .body(wall.render().unwrap_or_default().into())
.body(wall.render().unwrap_or_default().into()) .unwrap_or_default()
.unwrap_or_default(),
)
} }
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
let subreddit = req.param("sub").ok_or("Invalid URL")?; let subreddit = req.param("sub").ok_or("Invalid URL")?;
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?; let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
let mut response = redirect(redir); let mut response = redirect(&redir);
response.insert_cookie( response.insert_cookie(
Cookie::build((&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")) Cookie::build((&format!("allow_quaran_{}", subreddit.to_lowercase()), "true"))
.path("/") .path("/")
@ -206,9 +218,8 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
if sub == "random" || sub == "randnsfw" { if sub == "random" || sub == "randnsfw" {
if action.contains(&"filter".to_string()) || action.contains(&"unfilter".to_string()) { if action.contains(&"filter".to_string()) || action.contains(&"unfilter".to_string()) {
return Err("Can't filter random subreddit!".to_string()); return Err("Can't filter random subreddit!".to_string());
} else {
return Err("Can't subscribe to random subreddit!".to_string());
} }
return Err("Can't subscribe to random subreddit!".to_string());
} }
let query = req.uri().query().unwrap_or_default().to_string(); let query = req.uri().query().unwrap_or_default().to_string();
@ -219,7 +230,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
// Retrieve list of posts for these subreddits to extract display names // Retrieve list of posts for these subreddits to extract display names
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await; let posts = json(format!("/r/{sub}/hot.json?raw_json=1"), true).await;
let display_lookup: Vec<(String, &str)> = match &posts { let display_lookup: Vec<(String, &str)> = match &posts {
Ok(posts) => posts["data"]["children"] Ok(posts) => posts["data"]["children"]
.as_array() .as_array()
@ -247,7 +258,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
display display
} else { } else {
// This subreddit display name isn't known, retrieve it // This subreddit display name isn't known, retrieve it
let path: String = format!("/r/{}/about.json?raw_json=1", part); let path: String = format!("/r/{part}/about.json?raw_json=1");
display = json(path, true).await; display = json(path, true).await;
match &display { match &display {
Ok(display) => display["data"]["display_name"].as_str(), Ok(display) => display["data"]["display_name"].as_str(),
@ -282,13 +293,13 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
// Redirect back to subreddit // Redirect back to subreddit
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar // check for redirect parameter if unsubscribing/unfiltering from outside sidebar
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") { let path = if let Some(redirect_path) = param(&format!("?{query}"), "redirect") {
format!("/{}", redirect_path) format!("/{redirect_path}")
} else { } else {
format!("/r/{}", sub) format!("/r/{sub}")
}; };
let mut response = redirect(path); let mut response = redirect(&path);
// Delete cookie if empty, else set // Delete cookie if empty, else set
if sub_list.is_empty() { if sub_list.is_empty() {
@ -326,22 +337,22 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
} }
let page = req.param("page").unwrap_or_else(|| "index".to_string()); let page = req.param("page").unwrap_or_else(|| "index".to_string());
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page); let path: String = format!("/r/{sub}/wiki/{page}.json?raw_json=1");
let url = req.uri().to_string(); let url = req.uri().to_string();
match json(path, quarantined).await { match json(path, quarantined).await {
Ok(response) => template(WikiTemplate { Ok(response) => Ok(template(&WikiTemplate {
sub, sub,
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")), wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
page, page,
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url, url,
}), })),
Err(msg) => { Err(msg) => {
if msg == "quarantined" || msg == "gated" { if msg == "quarantined" || msg == "gated" {
quarantine(req, sub, msg) Ok(quarantine(&req, sub, &msg))
} else { } else {
error(req, msg).await error(req, &msg).await
} }
} }
} }
@ -357,13 +368,13 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
} }
// Build the Reddit JSON API url // Build the Reddit JSON API url
let path: String = format!("/r/{}/about.json?raw_json=1", sub); let path: String = format!("/r/{sub}/about.json?raw_json=1");
let url = req.uri().to_string(); let url = req.uri().to_string();
// Send a request to the url // Send a request to the url
match json(path, quarantined).await { match json(path, quarantined).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => template(WikiTemplate { Ok(response) => Ok(template(&WikiTemplate {
wiki: rewrite_urls(&val(&response, "description_html")), wiki: rewrite_urls(&val(&response, "description_html")),
// wiki: format!( // wiki: format!(
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>", // "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
@ -374,12 +385,12 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
page: "Sidebar".to_string(), page: "Sidebar".to_string(),
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url, url,
}), })),
Err(msg) => { Err(msg) => {
if msg == "quarantined" || msg == "gated" { if msg == "quarantined" || msg == "gated" {
quarantine(req, sub, msg) Ok(quarantine(&req, sub, &msg))
} else { } else {
error(req, msg).await error(req, &msg).await
} }
} }
} }
@ -422,7 +433,7 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
// SUBREDDIT // SUBREDDIT
async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> { async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
// Build the Reddit JSON API url // Build the Reddit JSON API url
let path: String = format!("/r/{}/about.json?raw_json=1", sub); let path: String = format!("/r/{sub}/about.json?raw_json=1");
// Send a request to the url // Send a request to the url
let res = json(path, quarantined).await?; let res = json(path, quarantined).await?;

View File

@ -35,9 +35,8 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
// Build the Reddit JSON API path // Build the Reddit JSON API path
let path = format!( let path = format!(
"/user/{}/{}.json?{}&raw_json=1", "/user/{}/{listing}.json?{}&raw_json=1",
req.param("name").unwrap_or_else(|| "reddit".to_string()), req.param("name").unwrap_or_else(|| "reddit".to_string()),
listing,
req.uri().query().unwrap_or_default(), req.uri().query().unwrap_or_default(),
); );
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
@ -60,11 +59,11 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
let filters = get_filters(&req); let filters = get_filters(&req);
if filters.contains(&["u_", &username].concat()) { if filters.contains(&["u_", &username].concat()) {
template(UserTemplate { Ok(template(&UserTemplate {
user, user,
posts: Vec::new(), posts: Vec::new(),
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after").unwrap_or_default(), "".to_string()), ends: (param(&path, "after").unwrap_or_default(), String::new()),
listing, listing,
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url, url,
@ -73,7 +72,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false, no_posts: false,
}) }))
} else { } else {
// Request user posts/comments from Reddit // Request user posts/comments from Reddit
match Post::fetch(&path, false).await { match Post::fetch(&path, false).await {
@ -81,7 +80,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty(); let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(UserTemplate { Ok(template(&UserTemplate {
user, user,
posts, posts,
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
@ -94,10 +93,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts, no_posts,
}) }))
} }
// If there is an error show error page // If there is an error show error page
Err(msg) => error(req, msg).await, Err(msg) => error(req, &msg).await,
} }
} }
} }
@ -105,7 +104,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
// USER // USER
async fn user(name: &str) -> Result<User, String> { async fn user(name: &str) -> Result<User, String> {
// Build the Reddit JSON API path // Build the Reddit JSON API path
let path: String = format!("/user/{}/about.json?raw_json=1", name); let path: String = format!("/user/{name}/about.json?raw_json=1");
// Send a request to the url // Send a request to the url
json(path, false).await.map(|res| { json(path, false).await.map(|res| {

View File

@ -1,3 +1,4 @@
#![allow(dead_code)]
use crate::config::get_setting; use crate::config::get_setting;
// //
// CRATES // CRATES
@ -6,6 +7,7 @@ use crate::{client::json, server::RequestExt};
use askama::Template; use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use log::error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
@ -115,8 +117,8 @@ impl Poll {
Some(Self { Some(Self {
poll_options, poll_options,
total_vote_count,
voting_end_timestamp, voting_end_timestamp,
total_vote_count,
}) })
} }
@ -155,6 +157,7 @@ impl PollOption {
// Post flags with nsfw and stickied // Post flags with nsfw and stickied
pub struct Flags { pub struct Flags {
pub spoiler: bool,
pub nsfw: bool, pub nsfw: bool,
pub stickied: bool, pub stickied: bool,
} }
@ -326,9 +329,8 @@ impl Post {
}; };
// Fetch the list of posts from the JSON response // Fetch the list of posts from the JSON response
let post_list = match res["data"]["children"].as_array() { let Some(post_list) = res["data"]["children"].as_array() else {
Some(list) => list, return Err("No posts found".to_string());
None => return Err("No posts found".to_string()),
}; };
let mut posts: Vec<Self> = Vec::new(); let mut posts: Vec<Self> = Vec::new();
@ -383,7 +385,7 @@ impl Post {
alt_url: String::new(), alt_url: String::new(),
width: data["thumbnail_width"].as_i64().unwrap_or_default(), width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(), height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: "".to_string(), poster: String::new(),
}, },
media, media,
domain: val(post, "domain"), domain: val(post, "domain"),
@ -402,6 +404,7 @@ impl Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: data["spoiler"].as_bool().unwrap_or_default(),
nsfw: data["over_18"].as_bool().unwrap_or_default(), nsfw: data["over_18"].as_bool().unwrap_or_default(),
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(), stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
}, },
@ -456,7 +459,7 @@ pub struct Award {
} }
impl std::fmt::Display for Award { impl std::fmt::Display for Award {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.name, self.icon_url, self.description) write!(f, "{} {} {}", self.name, self.icon_url, self.description)
} }
} }
@ -472,8 +475,8 @@ 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().fold(Ok(()), |result, award| result.and_then(|()| writeln!(f, "{award}")))
} }
} }
@ -570,14 +573,19 @@ pub struct Params {
#[derive(Default)] #[derive(Default)]
pub struct Preferences { pub struct Preferences {
pub available_themes: Vec<String>, pub available_themes: Vec<String>,
pub available_mascots: Vec<String>,
pub theme: String, pub theme: String,
pub mascot: String,
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
pub blur_spoiler: String,
pub show_nsfw: String, pub show_nsfw: String,
pub blur_nsfw: String, pub blur_nsfw: String,
pub hide_hls_notification: String, pub hide_hls_notification: String,
pub hide_sidebar_and_summary: String,
pub use_hls: String, pub use_hls: String,
pub ffmpeg_video_downloads: String,
pub autoplay_videos: String, pub autoplay_videos: String,
pub fixed_navbar: String, pub fixed_navbar: String,
pub disable_visit_reddit_confirmation: String, pub disable_visit_reddit_confirmation: String,
@ -594,6 +602,11 @@ pub struct Preferences {
#[include = "*.css"] #[include = "*.css"]
pub struct ThemeAssets; pub struct ThemeAssets;
#[derive(RustEmbed)]
#[folder = "static/mascots/"]
#[include = "*.png"]
pub struct MascotAssets;
impl Preferences { impl Preferences {
// Build preferences from cookies // Build preferences from cookies
pub fn new(req: &Request<Body>) -> Self { pub fn new(req: &Request<Body>) -> Self {
@ -602,17 +615,29 @@ impl Preferences {
let mut themes = vec!["system".to_string()]; let mut themes = vec!["system".to_string()];
for file in ThemeAssets::iter() { for file in ThemeAssets::iter() {
let chunks: Vec<&str> = file.as_ref().split(".css").collect(); let chunks: Vec<&str> = file.as_ref().split(".css").collect();
themes.push(chunks[0].to_owned()) themes.push(chunks[0].to_owned());
}
// Read available mascot names from embedded png files.
// Always make default "none" option available.
let mut mascots = vec!["none".to_string()];
for file in MascotAssets::iter() {
let chunks: Vec<&str> = file.as_ref().split(".png").collect();
mascots.push(chunks[0].to_owned());
} }
Self { Self {
available_themes: themes, available_themes: themes,
available_mascots: mascots,
theme: setting(req, "theme"), theme: setting(req, "theme"),
mascot: setting(req, "mascot"),
front_page: setting(req, "front_page"), front_page: setting(req, "front_page"),
layout: setting(req, "layout"), layout: setting(req, "layout"),
wide: setting(req, "wide"), wide: setting(req, "wide"),
blur_spoiler: setting(req, "blur_spoiler"),
show_nsfw: setting(req, "show_nsfw"), show_nsfw: setting(req, "show_nsfw"),
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
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"),
hide_hls_notification: setting(req, "hide_hls_notification"), hide_hls_notification: setting(req, "hide_hls_notification"),
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()),
@ -656,7 +681,7 @@ pub fn filter_posts(posts: &mut Vec<Post>, filters: &HashSet<String>) -> (u64, b
} }
/// Creates a [`Post`] from a provided JSON. /// Creates a [`Post`] from a provided JSON.
pub async fn parse_post(post: &serde_json::Value) -> Post { pub async fn parse_post(post: &Value) -> Post {
// Grab UTC time as unix timestamp // Grab UTC time as unix timestamp
let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default()); let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default());
// Parse post score and upvote ratio // Parse post score and upvote ratio
@ -674,9 +699,8 @@ pub async fn parse_post(post: &serde_json::Value) -> Post {
let body = if val(post, "removed_by_category") == "moderator" { let body = if val(post, "removed_by_category") == "moderator" {
format!( format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}\">view removed post</a></p></div>", "<div class=\"md\"><p>[removed] — <a href=\"https://{}{permalink}\">view removed post</a></p></div>",
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)), get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
permalink
) )
} else { } else {
rewrite_urls(&val(post, "selftext_html")) rewrite_urls(&val(post, "selftext_html"))
@ -730,6 +754,7 @@ pub async fn parse_post(post: &serde_json::Value) -> Post {
}, },
}, },
flags: Flags { flags: Flags {
spoiler: post["data"]["spoiler"].as_bool().unwrap_or_default(),
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(), nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false), stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
}, },
@ -752,7 +777,7 @@ pub async fn parse_post(post: &serde_json::Value) -> Post {
// Grab a query parameter from a url // Grab a query parameter from a url
pub fn param(path: &str, value: &str) -> Option<String> { pub fn param(path: &str, value: &str) -> Option<String> {
Some( Some(
Url::parse(format!("https://libredd.it/{}", path).as_str()) Url::parse(format!("https://libredd.it/{path}").as_str())
.ok()? .ok()?
.query_pairs() .query_pairs()
.into_owned() .into_owned()
@ -769,7 +794,7 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
.cookie(name) .cookie(name)
.unwrap_or_else(|| { .unwrap_or_else(|| {
// If there is no cookie for this setting, try receiving a default from the config // If there is no cookie for this setting, try receiving a default from the config
if let Some(default) = crate::config::get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) { if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) {
Cookie::new(name, default) Cookie::new(name, default)
} else { } else {
Cookie::from(name) Cookie::from(name)
@ -782,21 +807,21 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
// Retrieve the value of a setting by name or the default value // Retrieve the value of a setting by name or the default value
pub fn setting_or_default(req: &Request<Body>, name: &str, default: String) -> String { pub fn setting_or_default(req: &Request<Body>, name: &str, default: String) -> String {
let value = setting(req, name); let value = setting(req, name);
if !value.is_empty() { if value.is_empty() {
value
} else {
default default
} else {
value
} }
} }
// Detect and redirect in the event of a random subreddit // Detect and redirect in the event of a random subreddit
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> { pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
if sub == "random" || sub == "randnsfw" { if sub == "random" || sub == "randnsfw" {
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"] let new_sub = json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"]
.as_str() .as_str()
.unwrap_or_default() .unwrap_or_default()
.to_string(); .to_string();
Ok(redirect(format!("/r/{}{}", new_sub, additional))) Ok(redirect(&format!("/r/{new_sub}{additional}")))
} else { } else {
Err("No redirect needed".to_string()) Err("No redirect needed".to_string())
} }
@ -876,16 +901,18 @@ pub fn format_url(url: &str) -> String {
// These are links we want to replace in-body // These are links we want to replace in-body
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap()); static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap());
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview)\.redd\.it(.*)[^?]").unwrap()); static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap());
static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap()); static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap());
static REDLIB_PREVIEW_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"/(img|preview/)(pre|external-pre)?/(.*?)>"#).unwrap());
static REDLIB_PREVIEW_TEXT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r">(.*?)</a>").unwrap());
// Rewrite Reddit links to Redlib in body of text // Rewrite Reddit links to Redlib in body of text
pub fn rewrite_urls(input_text: &str) -> String { pub fn rewrite_urls(input_text: &str) -> String {
let text1 = let mut text1 =
// 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();
let text1 = REDDIT_EMOJI_REGEX text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default())) .replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string() .to_string()
// Remove (html-encoded) "\" from URLs. // Remove (html-encoded) "\" from URLs.
@ -893,12 +920,56 @@ pub fn rewrite_urls(input_text: &str) -> String {
.replace("\\_", "_"); .replace("\\_", "_");
// Rewrite external media previews to Redlib // Rewrite external media previews to Redlib
if REDDIT_PREVIEW_REGEX.is_match(&text1) { loop {
REDDIT_PREVIEW_REGEX if REDDIT_PREVIEW_REGEX.find(&text1).is_none() {
.replace_all(&text1, format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default())) return text1;
.to_string() } else {
} else { let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default());
text1
let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
/* As long as image_caption isn't empty remove first and last four characters of image_text to leave us with just the text in the caption without any HTML.
This makes it possible to enclose it in a <figcaption> later on without having stray HTML breaking it */
if !image_caption.is_empty() {
image_caption = image_caption[1..image_caption.len() - 4].to_string();
}
// image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them
let image_to_replace = format!("<a href=\"{image_url}{image_caption}</a>");
// _image_replacement needs to be in scope for the replacement at the bottom of the loop
let mut _image_replacement = String::new();
/* We don't want to show a caption that's just the image's link, so we check if we find a Reddit preview link within the image's caption.
If we don't find one we must have actual text, so we include a <figcaption> block that contains it.
Otherwise we don't include the <figcaption> block as we don't need it. */
if REDDIT_PREVIEW_REGEX.find(&image_caption).is_none() {
// Without this " would show as \" instead. "\&quot;" is how the quotes are formatted within image_text beforehand
image_caption = image_caption.replace("\\&quot;", "\"");
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a><figcaption>{image_caption}</figcaption></figure>");
} else {
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a></figure>");
}
/* In order to know if we're dealing with a normal or external preview we need to take a look at the first capture group of REDDIT_PREVIEW_REGEX
if it's preview we're dealing with something that needs /preview/pre, external-preview is /preview/external-pre, and i is /img */
let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()).to_string();
let mut _preview_type = String::new();
if reddit_preview_regex_capture == "preview" {
_preview_type = "/preview/pre".to_string();
} else if reddit_preview_regex_capture == "external-preview" {
_preview_type = "/preview/external-pre".to_string();
} else {
_preview_type = "/img".to_string();
}
text1 = REDDIT_PREVIEW_REGEX
.replace(&text1, format!("{_preview_type}$2"))
.replace(&image_to_replace, &_image_replacement)
.to_string()
}
} }
} }
@ -962,27 +1033,26 @@ pub fn val(j: &Value, k: &str) -> String {
// NETWORKING // NETWORKING
// //
pub fn template(t: impl Template) -> Result<Response<Body>, String> { pub fn template(t: &impl Template) -> Response<Body> {
Ok( Response::builder()
Response::builder() .status(200)
.status(200) .header("content-type", "text/html")
.header("content-type", "text/html") .body(t.render().unwrap_or_default().into())
.body(t.render().unwrap_or_default().into()) .unwrap_or_default()
.unwrap_or_default(),
)
} }
pub fn redirect(path: String) -> Response<Body> { pub fn redirect(path: &str) -> Response<Body> {
Response::builder() Response::builder()
.status(302) .status(302)
.header("content-type", "text/html") .header("content-type", "text/html")
.header("Location", &path) .header("Location", path)
.body(format!("Redirecting to <a href=\"{0}\">{0}</a>...", path).into()) .body(format!("Redirecting to <a href=\"{path}\">{path}</a>...").into())
.unwrap_or_default() .unwrap_or_default()
} }
/// Renders a generic error landing page. /// Renders a generic error landing page.
pub async fn error(req: Request<Body>, msg: impl ToString) -> Result<Response<Body>, String> { pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
let url = req.uri().to_string(); let url = req.uri().to_string();
let body = ErrorTemplate { let body = ErrorTemplate {
msg: msg.to_string(), msg: msg.to_string(),
@ -1003,7 +1073,7 @@ pub async fn error(req: Request<Body>, msg: impl ToString) -> Result<Response<Bo
/// subreddits or posts or userpages for users Reddit has deemed NSFW will /// subreddits or posts or userpages for users Reddit has deemed NSFW will
/// be denied. /// be denied.
pub fn sfw_only() -> bool { pub fn sfw_only() -> bool {
match crate::config::get_setting("REDLIB_SFW_ONLY") { match get_setting("REDLIB_SFW_ONLY") {
Some(val) => val == "on", Some(val) => val == "on",
None => false, None => false,
} }
@ -1027,7 +1097,7 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
// Determine from the request URL if the resource is a subreddit, a user // Determine from the request URL if the resource is a subreddit, a user
// page, or a post. // page, or a post.
let res: String = if !req.param("name").unwrap_or_default().is_empty() { let resource: String = if !req.param("name").unwrap_or_default().is_empty() {
res_type = ResourceType::User; res_type = ResourceType::User;
req.param("name").unwrap_or_default() req.param("name").unwrap_or_default()
} else if !req.param("id").unwrap_or_default().is_empty() { } else if !req.param("id").unwrap_or_default().is_empty() {
@ -1039,7 +1109,7 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
}; };
let body = NSFWLandingTemplate { let body = NSFWLandingTemplate {
res, res: resource,
res_type, res_type,
prefs: Preferences::new(&req), prefs: Preferences::new(&req),
url: req_url, url: req_url,
@ -1150,3 +1220,11 @@ async fn test_fetching_ws() {
assert!(post.ws_url.starts_with("wss://k8s-lb.wss.redditmedia.com/link/")); assert!(post.ws_url.starts_with("wss://k8s-lb.wss.redditmedia.com/link/"));
} }
} }
#[test]
fn test_rewriting_image_links() {
let input =
r#"<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>"#;
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&amp;format=png&amp;auto=webp&amp;s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
assert_eq!(rewrite_urls(input), output);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(a){var o=t[a];if(void 0!==o)return o.exports;var s=t[a]={exports:{}};return e[a](s,s.exports,r),s.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(e||(e={}));const t=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),o=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let s;self.onmessage=async({data:{id:n,type:E,data:i}})=>{const c=[];let p;try{if(E!==e.LOAD&&!s)throw a;switch(E){case e.LOAD:p=await(async({coreURL:t="https://unpkg.com/@ffmpeg/core@0.12.1/dist/umd/ffmpeg-core.js",wasmURL:a,workerURL:n})=>{const E=!s,i=t,c=a||t.replace(/.js$/g,".wasm"),p=n||t.replace(/.js$/g,".worker.js");try{importScripts(i)}catch{if(self.createFFmpegCore=(await r(454)(i)).default,!self.createFFmpegCore)throw o}return s=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:c,workerURL:p}))}`}),s.setLogger((t=>self.postMessage({type:e.LOG,data:t}))),s.setProgress((t=>self.postMessage({type:e.PROGRESS,data:t}))),E})(i);break;case e.EXEC:p=(({args:e,timeout:t=-1})=>{s.setTimeout(t),s.exec(...e);const r=s.ret;return s.reset(),r})(i);break;case e.WRITE_FILE:p=(({path:e,data:t})=>(s.FS.writeFile(e,t),!0))(i);break;case e.READ_FILE:p=(({path:e,encoding:t})=>s.FS.readFile(e,{encoding:t}))(i);break;case e.DELETE_FILE:p=(({path:e})=>(s.FS.unlink(e),!0))(i);break;case e.RENAME:p=(({oldPath:e,newPath:t})=>(s.FS.rename(e,t),!0))(i);break;case e.CREATE_DIR:p=(({path:e})=>(s.FS.mkdir(e),!0))(i);break;case e.LIST_DIR:p=(({path:e})=>{const t=s.FS.readdir(e),r=[];for(const a of t){const t=s.FS.stat(`${e}/${a}`),o=s.FS.isDir(t.mode);r.push({name:a,isDir:o})}return r})(i);break;case e.DELETE_DIR:p=(({path:e})=>(s.FS.rmdir(e),!0))(i);break;case e.MOUNT:p=(({fsType:e,options:t,mountPoint:r})=>{let a=e,o=s.FS.filesystems[a];return!!o&&(s.FS.mount(o,t,r),!0)})(i);break;case e.UNMOUNT:p=(({mountPoint:e})=>(s.FS.unmount(e),!0))(i);break;default:throw t}}catch(t){return void self.postMessage({id:n,type:e.ERROR,data:t.toString()})}p instanceof Uint8Array&&c.push(p.buffer),self.postMessage({id:n,type:E,data:p},c)}})(),{}})()));
//# sourceMappingURL=814.ffmpeg.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
static/ffmpeg/ffmpeg-core.wasm Executable file

Binary file not shown.

View File

@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegUtil=t():e.FFmpegUtil=t()}(self,(()=>(()=>{"use strict";var e={591:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.HeaderContentLength=void 0,t.HeaderContentLength="Content-Length"},431:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ERROR_INCOMPLETED_DOWNLOAD=t.ERROR_RESPONSE_BODY_READER=void 0,t.ERROR_RESPONSE_BODY_READER=new Error("failed to get response body reader"),t.ERROR_INCOMPLETED_DOWNLOAD=new Error("failed to complete download")},915:function(e,t,o){var r=this&&this.__awaiter||function(e,t,o,r){return new(o||(o=Promise))((function(n,i){function d(e){try{l(r.next(e))}catch(e){i(e)}}function a(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?n(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(d,a)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.toBlobURL=t.downloadWithProgress=t.importScript=t.fetchFile=void 0;const n=o(431),i=o(591);t.fetchFile=e=>r(void 0,void 0,void 0,(function*(){let t;if("string"==typeof e)t=/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?atob(e.split(",")[1]).split("").map((e=>e.charCodeAt(0))):yield(yield fetch(e)).arrayBuffer();else if(e instanceof URL)t=yield(yield fetch(e)).arrayBuffer();else{if(!(e instanceof File||e instanceof Blob))return new Uint8Array;t=yield(o=e,new Promise(((e,t)=>{const r=new FileReader;r.onload=()=>{const{result:t}=r;t instanceof ArrayBuffer?e(new Uint8Array(t)):e(new Uint8Array)},r.onerror=e=>{var o,r;t(Error(`File could not be read! Code=${(null===(r=null===(o=null==e?void 0:e.target)||void 0===o?void 0:o.error)||void 0===r?void 0:r.code)||-1}`))},r.readAsArrayBuffer(o)})))}var o;return new Uint8Array(t)})),t.importScript=e=>r(void 0,void 0,void 0,(function*(){return new Promise((t=>{const o=document.createElement("script"),r=()=>{o.removeEventListener("load",r),t()};o.src=e,o.type="text/javascript",o.addEventListener("load",r),document.getElementsByTagName("head")[0].appendChild(o)}))})),t.downloadWithProgress=(e,t)=>r(void 0,void 0,void 0,(function*(){var o;const r=yield fetch(e);let d;try{const a=parseInt(r.headers.get(i.HeaderContentLength)||"-1"),l=null===(o=r.body)||void 0===o?void 0:o.getReader();if(!l)throw n.ERROR_RESPONSE_BODY_READER;const c=[];let s=0;for(;;){const{done:o,value:r}=yield l.read(),i=r?r.length:0;if(o){if(-1!=a&&a!==s)throw n.ERROR_INCOMPLETED_DOWNLOAD;t&&t({url:e,total:a,received:s,delta:i,done:o});break}c.push(r),s+=i,t&&t({url:e,total:a,received:s,delta:i,done:o})}const f=new Uint8Array(s);let u=0;for(const e of c)f.set(e,u),u+=e.length;d=f.buffer}catch(o){console.log("failed to send download progress event: ",o),d=yield r.arrayBuffer(),t&&t({url:e,total:d.byteLength,received:d.byteLength,delta:0,done:!0})}return d})),t.toBlobURL=(e,o,n=!1,i)=>r(void 0,void 0,void 0,(function*(){const r=n?yield(0,t.downloadWithProgress)(e,i):yield(yield fetch(e)).arrayBuffer(),d=new Blob([r],{type:o});return URL.createObjectURL(d)}))}},t={};return function o(r){var n=t[r];if(void 0!==n)return n.exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,o),i.exports}(915)})()));

2
static/ffmpeg/ffmpeg.js Normal file
View File

@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=(s={},{signal:r}={})=>(this.#e||(this.#e=new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:s},void 0,r));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}return s})()));
//# sourceMappingURL=ffmpeg.js.map

File diff suppressed because one or more lines are too long

6
static/hls.min.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 60 KiB

33
static/logo.svg Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 512 512"
id="svg2"
width="512"
height="512"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs2" />
<rect width="512" height="512" fill="#4c082a" />
<g
transform="matrix(0.75272,0,0,0.75272,-1.1596187,-0.37987125)"
id="g2">
<circle
fill="#1a1a1a"
id="circle1"
style="fill:#4c082a;fill-opacity:0"
r="340.10001"
cy="340.32001"
cx="341.10999" />
<path
d="m 320.64,126.73 v 300.8 h 92.264 V 219.61 h 75.803 v -92.83 h -75.803 v -0.0508 z"
fill="#f83240"
id="path1"
style="fill:#f83240;fill-opacity:1" />
<path
d="M 193.1,126.74 V 510.7 h 0.006 v 43.543 h 295.82 v -92.338 h -202.74 v -335.16 z"
fill="#f83240"
id="path2"
style="fill:#f83240;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 943 B

BIN
static/logo_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,77 +0,0 @@
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
(function () {
if (Hls.isSupported()) {
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
videoSources.forEach(function (source) {
var playlist = source.src;
var oldVideo = source.parentNode;
var autoplay = oldVideo.classList.contains("hls_autoplay");
// If HLS is supported natively then don't use hls.js
if (oldVideo.canPlayType(source.type)) {
if (autoplay) {
oldVideo.play();
}
return;
}
// Replace video with copy that will have all "source" elements removed
var newVideo = oldVideo.cloneNode(true);
var allSources = newVideo.querySelectorAll("source");
allSources.forEach(function (source) {
source.remove();
});
// Empty source to enable play event
newVideo.src = "about:blank";
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
function initializeHls() {
newVideo.removeEventListener('play', initializeHls);
var hls = new Hls({ autoStartLoad: false });
hls.loadSource(playlist);
hls.attachMedia(newVideo);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
hls.loadLevel = hls.levels.length - 1;
hls.startLoad();
newVideo.play();
});
hls.on(Hls.Events.ERROR, function (event, data) {
var errorType = data.type;
var errorFatal = data.fatal;
if (errorFatal) {
switch (errorType) {
case Hls.ErrorType.NETWORK_ERROR:
hls.startLoad();
break;
case Hls.ErrorType.MEDIA_ERROR:
hls.recoverMediaError();
break;
default:
hls.destroy();
break;
}
}
console.error("HLS error", data);
});
}
newVideo.addEventListener('play', initializeHls);
if (autoplay) {
newVideo.play();
}
});
} else {
var videos = document.querySelectorAll("video.hls_autoplay");
videos.forEach(function (video) {
video.setAttribute("autoplay", "");
});
}
})();
// @license-end

View File

@ -34,6 +34,7 @@
font-family: 'Inter'; font-family: 'Inter';
src: url('/Inter.var.woff2') format('woff2-variations'); src: url('/Inter.var.woff2') format('woff2-variations');
font-style: normal; font-style: normal;
font-weight: 100 900;
} }
/* Automatic theme selection */ /* Automatic theme selection */
@ -51,6 +52,7 @@
--visited: #aaa; --visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5); --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
--popup: #b80a27; --popup: #b80a27;
--spoiler: #ddd;
/* Hint color theme to browser for scrollbar */ /* Hint color theme to browser for scrollbar */
color-scheme: dark; color-scheme: dark;
@ -70,6 +72,7 @@
--highlighted: white; --highlighted: white;
--visited: #555; --visited: #555;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--spoiler: #0f0f0f;
/* Hint color theme to browser for scrollbar */ /* Hint color theme to browser for scrollbar */
color-scheme: light; color-scheme: light;
@ -112,16 +115,30 @@ pre, form, fieldset, table, th, td, select, input {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }
html.fixed_navbar {
scroll-padding-top: 50px;
}
@media screen and (max-width: 800px) {
html.fixed_navbar {
scroll-padding-top: 100px;
}
}
body { body {
background: var(--background); background: var(--background);
padding-bottom: var(--footer-height);
font-size: 15px; font-size: 15px;
position: relative;
}
body.card {
min-height: calc(100vh - 30px);
} }
body.fixed_navbar { body.fixed_navbar {
min-height: calc(100vh - 90px);
padding-top: 60px; padding-top: 60px;
padding-bottom: var(--footer-height);
min-height: calc(100vh - 60px);
position: relative;
} }
nav { nav {
@ -173,6 +190,11 @@ nav #redlib {
vertical-align: -2px; vertical-align: -2px;
} }
figcaption {
margin-top: 5px;
text-align: center;
}
#settings_link { #settings_link {
opacity: 0.8; opacity: 0.8;
margin-left: 10px; margin-left: 10px;
@ -909,6 +931,15 @@ a.search_subreddit:hover {
font-weight: bold; font-weight: bold;
} }
.spoiler {
color: var(--spoiler);
margin-left: 5px;
border: 1px solid var(--spoiler);
padding: 3px;
font-size: 12px;
border-radius: 5px;
}
.post_media_content, .post .__NoScript_PlaceHolder__, .gallery { .post_media_content, .post .__NoScript_PlaceHolder__, .gallery {
max-width: calc(100% - 40px); max-width: calc(100% - 40px);
grid-area: post_media; grid-area: post_media;
@ -965,10 +996,6 @@ a.search_subreddit:hover {
vertical-align: bottom; vertical-align: bottom;
} }
.gallery figcaption {
margin-top: 5px;
}
.gallery .outbound_url { .gallery .outbound_url {
color: var(--accent); color: var(--accent);
text-overflow: ellipsis; text-overflow: ellipsis;
@ -994,6 +1021,13 @@ a.search_subreddit:hover {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.post_body img {
max-width: 100%;
display: block;
margin-left: auto;
margin-right: auto;
}
.post_poll { .post_poll {
grid-area: post_poll; grid-area: post_poll;
padding: 5px 15px 5px 12px; padding: 5px 15px 5px 12px;
@ -1080,7 +1114,7 @@ a.search_subreddit:hover {
display: auto; display: auto;
} }
@media screen and (min-width: 480px) { @media screen and (min-width: 481px) {
#post_links > li.mobile_item { #post_links > li.mobile_item {
display: none; display: none;
} }
@ -1157,6 +1191,22 @@ a.search_subreddit:hover {
display: flex; display: flex;
} }
.comment img {
max-width: 50%;
height: auto;
}
@media screen and (max-width: 500px) {
.comment img {
max-width: 80%;
height: auto;
}
}
.comment figure {
margin: 0;
}
.comment_left, .comment_right { .comment_left, .comment_right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1352,6 +1402,11 @@ summary.comment_data {
font-size: 14px; font-size: 14px;
margin-top: 10px; margin-top: 10px;
opacity: 0.75; opacity: 0.75;
background: var(--post);
border-radius: 5px;
padding: 10px 5px 10px 10px;
margin-top: 10px;
margin-bottom: 20px;
} }
#settings_note a { #settings_note a {
@ -1402,6 +1457,7 @@ summary.comment_data {
box-shadow: var(--shadow); box-shadow: var(--shadow);
margin-left: 20px; margin-left: 20px;
background: var(--foreground); background: var(--foreground);
width: 30%;
} }
aside.prefs { aside.prefs {
@ -1454,10 +1510,19 @@ input[type="submit"] {
width: 100%; width: 100%;
} }
.md > *:not(:first-child) { .md > p:not(:first-child):not(:last-child) {
margin-top: 20px; margin-top: 20px;
} }
.md > figure:first-of-type {
margin-top: 5px;
margin-bottom: 0px;
}
.md > figure:not(:first-of-type) {
margin-top: 10px;
}
.md h1 { font-size: 22px; } .md h1 { font-size: 22px; }
.md h2 { font-size: 20px; } .md h2 { font-size: 20px; }
.md h3 { font-size: 18px; } .md h3 { font-size: 18px; }
@ -1585,6 +1650,14 @@ td, th {
color: var(--accent); color: var(--accent);
} }
.nsfw-tag {
color: var(--nsfw);
border: 2px solid var(--nsfw);
padding: 3px;
border-radius: 5px;
font-weight: bold;
}
/* Mobile */ /* Mobile */
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
@ -1710,3 +1783,54 @@ td, th {
justify-content: initial; justify-content: initial;
} }
} }
.video-options {
border: 2px var(--outside) solid;
margin-top: 8px;
float: right;
border-radius: 5px;
height: 35px;
height: 35px;
margin: 2px;
box-sizing: border-box;
}
.video-options option {
background-color: var(--background);
color: var(--text);
}
.video-options option:hover {
background-color: var(--accent);
color: var(--text);
}
.mascot {
position: fixed;
right: 1em;
bottom: 1em;
pointer-events: none;
opacity: 0.5;
z-index: 1;
}
.mascot > img {
max-width: 20em;
}
.download {
padding-left: 8px;
padding-right: 8px;
font-size: 20px;
font-weight: 900;
color: var(--accent);
background-color: var(--outside);
}
.download:hover {
background-color: var(--foreground);
/*color: var(--);*/
}
.download:active {
background-color: var(--background);
}

View File

@ -0,0 +1,17 @@
/* Catppuccin theme setting */
.catppuccin {
--accent: #b4befe; /* lavender */
--green: #a6e3a1; /* green */
--text: #cdd6f4; /* text */
--foreground: #181825; /* mantle */
--background: #1e1e2e; /* base */
--outside: #11111b; /* crust */
--post: #11111b; /* crust */
--panel-border: none;
--highlighted: #313244; /* surface0 */
--visited: #6c7086; /* overlay0 */
--shadow: 0 0 0 transparent;
--nsfw: #fab387; /* peach */
--admin: #eba0ac; /* maroon */
}

View File

@ -0,0 +1,14 @@
/* icebergDark theme setting */
.icebergDark {
--accent: #85a0c7;
--green: #b5bf82;
--text: #c6c8d1;
--foreground: #454d73;
--background: #161821;
--outside: #1f2233;
--post: #1f2233;
--panel-border: 1px solid #454d73;
--highlighted: #0f1117;
--visited: #0f1117;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

228
static/videoUtils.js Normal file
View File

@ -0,0 +1,228 @@
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
let ffmpeg = null;
(function () {
if (Hls.isSupported()) {
var downloadsEnabled = document.cookie.split("; ").find((row) => row.startsWith("ffmpeg_video_downloads="))?.split("=")[1] == "on";
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
videoSources.forEach(function (source) {
var playlist = source.src;
var oldVideo = source.parentNode;
var autoplay = oldVideo.classList.contains("hls_autoplay");
// If HLS is supported natively then don't use hls.js
if (oldVideo.canPlayType(source.type) === "probably") {
if (autoplay) {
oldVideo.play();
}
return;
}
// Replace video with copy that will have all "source" elements removed
var newVideo = oldVideo.cloneNode(true);
var allSources = newVideo.querySelectorAll("source");
allSources.forEach(function (source) {
source.remove();
});
// Empty source to enable play event
newVideo.src = "about:blank";
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
function initializeHls() {
newVideo.removeEventListener('play', initializeHls);
var hls = new Hls({ autoStartLoad: false });
hls.loadSource(playlist);
hls.attachMedia(newVideo);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
hls.loadLevel = hls.levels.length - 1;
var availableLevels = hls.levels.map(function(level) {
return {
height: level.height,
width: level.width,
bitrate: level.bitrate,
};
});
addQualitySelector(newVideo, hls, availableLevels);
if (downloadsEnabled){ addVideoDownload(newVideo, hls); }
hls.startLoad();
newVideo.play();
});
hls.on(Hls.Events.ERROR, function (event, data) {
var errorType = data.type;
var errorFatal = data.fatal;
if (errorFatal) {
switch (errorType) {
case Hls.ErrorType.NETWORK_ERROR:
hls.startLoad();
break;
case Hls.ErrorType.MEDIA_ERROR:
hls.recoverMediaError();
break;
default:
hls.destroy();
break;
}
}
console.error("HLS error", data);
});
}
if (downloadsEnabled){
const { fetchFile } = FFmpegUtil;
const { FFmpeg } = FFmpegWASM;
function addVideoDownload(videoElement, hlsInstance) {
var mediaStream = [];
var downloadButton = document.createElement('button');
downloadButton.classList.add('video-options','download');
downloadButton.innerText = "⏳"
const mergeStreams = async () => {
if (ffmpeg === null) {
ffmpeg = new FFmpeg();
await ffmpeg.load({
coreURL: "/ffmpeg/ffmpeg-core.js",
});
ffmpeg.on("log", ({ message }) => {
console.log(message); // This is quite noisy but i will include it
})
ffmpeg.on("progress", ({ progress, time }) => { // Progress TODO: show progress ring around button not just ⏳
// console.log("ffmpeg prog:",progress * 100)
});
}
// Combine Video Audio Streams
await ffmpeg.writeFile("video", await fetchFile(concatBlob(mediaStream['video'])));
await ffmpeg.writeFile("audio", await fetchFile(concatBlob(mediaStream['audio'])));
console.time('ffmpeg-exec');
await ffmpeg.exec(['-i', "video", '-i', "audio",'-c:v', "copy", '-c:a', "aac", 'output.mp4']);
console.timeEnd('ffmpeg-exec')
// Save
const toSlug = (str) => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[\W_]+/g, '-')
.toLowerCase()
.replace(/^-+|-+$/g, '');
}
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
const data = await ffmpeg.readFile('output.mp4');
saveAs(new Blob([data.buffer]),filename);
return
}
function saveAs(blob, filename) { // Yeah ok...
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
}
function concatBlob(inputArray) {
var totalLength = inputArray.reduce(function (prev, cur) {
return prev + cur.length
}, 0);
var result = new Uint8Array(totalLength);
var offset = 0;
inputArray.forEach(function (element) {
result.set(element, offset);
offset += element.length;
});
return new Blob([result], {
type: 'application/octet-stream'
});
}
function getStreams() {
var video = document.createElement('video');
video.autoplay = true;
var dataStreams = {
'video': [],
'audio': []
};
mediaStream = dataStreams; // Update stream
hlsInstance.on(Hls.Events.BUFFER_APPENDING, function (event, data) {
dataStreams[data.type].push(data.data);
});
var isDownloading = false
function startDownload() {
if (!isDownloading) { isDownloading = true } else { return }
downloadButton.innerText = "⏳"
mergeStreams()
.then(_ => {
isDownloading = false
downloadButton.innerText = "⭳"
});
}
function waitForLoad() {
const poll = resolve => {
if(hlsInstance._media.buffered.length === 1 &&
hlsInstance._media.buffered.start(0) === 0 &&
hlsInstance._media.buffered.end(0) === hlsInstance._media.duration)
resolve();
else setTimeout(_ => poll(resolve), 400);
}
return new Promise(poll);
}
waitForLoad(_ => flag === true)
.then(_ => {
downloadButton.innerText = "⭳"
downloadButton.addEventListener('click', startDownload);
});
}
videoElement.parentNode.appendChild(downloadButton);
getStreams()
}
}
function addQualitySelector(videoElement, hlsInstance, availableLevels) {
var qualitySelector = document.createElement('select');
qualitySelector.classList.add('video-options');
var last = availableLevels.length - 1;
availableLevels.forEach(function (level, index) {
var option = document.createElement('option');
option.value = index.toString();
var bitrate = (level.bitrate / 1_000).toFixed(0);
option.text = level.height + 'p (' + bitrate + ' kbps)';
if (index === last) {
option.selected = "selected";
}
qualitySelector.appendChild(option);
});
qualitySelector.selectedIndex = availableLevels.length - 1;
qualitySelector.addEventListener('change', function () {
var selectedIndex = qualitySelector.selectedIndex;
hlsInstance.nextLevel = selectedIndex;
hlsInstance.startLoad();
});
videoElement.parentNode.appendChild(qualitySelector);
}
newVideo.addEventListener('play', initializeHls);
if (autoplay) {
newVideo.play();
}
});
} else {
var videos = document.querySelectorAll("video.hls_autoplay");
videos.forEach(function (video) {
video.setAttribute("autoplay", "");
});
}
})();
// @license-end

View File

@ -1,7 +1,7 @@
{% import "utils.html" as utils %} {% import "utils.html" as utils %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="{% if prefs.fixed_navbar == "on" %}fixed_navbar{% endif %}">
<head> <head>
{% block head %} {% block head %}
<title>{% block title %}Redlib{% endblock %}</title> <title>{% block title %}Redlib{% endblock %}</title>
@ -35,7 +35,7 @@
<nav class=" <nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}"> {% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<div id="logo"> <div id="logo">
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">lib.</span></a> <a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib</span></a>
{% block subscriptions %}{% endblock %} {% block subscriptions %}{% endblock %}
</div> </div>
{% block search %}{% endblock %} {% block search %}{% endblock %}
@ -59,6 +59,13 @@
</a> </a>
</div> </div>
</nav> </nav>
{% if prefs.mascot != "none" && prefs.mascot != "" %}
<!-- MASCOT -->
<div class="mascot">
<img src="/mascot/{{ prefs.mascot }}.png">
</div>
{% endif %}
<!-- MAIN CONTENT --> <!-- MAIN CONTENT -->
{% block body %} {% block body %}
@ -76,7 +83,7 @@
<a href="/info" title="View instance information">ⓘ View instance info</a> <a href="/info" title="View instance information">ⓘ View instance info</a>
</div> </div>
<div class="footer-button"> <div class="footer-button">
<a href="https://github.com/redlib-org/redlib" title="View code on GitHub">&lt;&gt; Code</a> <a href="https://git.stardust.wtf/iridium/redsunlib" title="View code on git.stardust.wtf">&lt;&gt; Code</a>
</div> </div>
</footer> </footer>
{% endblock %} {% endblock %}

View File

@ -2,10 +2,14 @@
{% block title %}Error: {{ msg }}{% endblock %} {% block title %}Error: {{ msg }}{% endblock %}
{% block sortstyle %}{% endblock %} {% block sortstyle %}{% endblock %}
{% block content %} {% block content %}
<div id="error"> <div id="error">
<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>Head back <a href="/">home</a>?</h3> <h3>Expected something to work? <a
</div> href="https://github.com/redlib-org/redlib/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=%F0%9F%90%9B+Bug+Report%3A+{{ msg }}">Report
an issue</a></h3>
<br />
<h3>Head back <a href="/">home</a>?</h3>
</div>
{% endblock %} {% endblock %}

View File

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

View File

@ -97,9 +97,13 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if prefs.use_hls == "on" %} {% if prefs.ffmpeg_video_downloads == "on" %}
<script src="/ffmpeg/ffmpeg.js"></script>
<script src="/ffmpeg/ffmpeg-util.js"></script>
{% endif %}
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script> <script src="/videoUtils.js"></script>
{% endif %} {% endif %}
{% if params.typed != "sr_user" %} {% if params.typed != "sr_user" %}

View File

@ -19,6 +19,12 @@
{% call utils::options(prefs.theme, prefs.available_themes, "system") %} {% call utils::options(prefs.theme, prefs.available_themes, "system") %}
</select> </select>
</div> </div>
<div class="prefs-group">
<label for="mascot">Mascot:</label>
<select name="mascot" id="mascot">
{% call utils::options(prefs.mascot, prefs.available_mascots, "system") %}
</select>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Interface</legend> <legend>Interface</legend>
@ -54,6 +60,11 @@
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select> </select>
</div> </div>
<div class="prefs-group">
<label for="blur_spoiler">Blur spoiler previews:</label>
<input type="hidden" value="off" name="blur_spoiler">
<input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}>
</div>
{% if !crate::utils::sfw_only() %} {% if !crate::utils::sfw_only() %}
<div class="prefs-group"> <div class="prefs-group">
<label for="show_nsfw">Show NSFW posts:</label> <label for="show_nsfw">Show NSFW posts:</label>
@ -71,19 +82,36 @@
<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"> <div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label> <label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar"> <input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}> <input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div> </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" %}
<details id="feeds"> <details id="feeds">
<summary>Why?</summary> <summary>Why?</summary>
<div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div> <div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div>
</details> </details>
{% endif %}
{% if prefs.ffmpeg_video_downloads == "on" %}<u>ⓘ HLS is required for downloads</u>{% endif %}
<input type="hidden" value="off" name="use_hls"> <input type="hidden" value="off" name="use_hls">
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}> <input type="checkbox" name="use_hls" id="use_hls" {% if prefs.ffmpeg_video_downloads == "on" %}disabled{% endif %} {% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="ffmpeg_video_downloads">Use FFmpeg to download videos</label>
<details id="feeds">
<summary>Why?</summary>
<div id="feed_list" class="helper">Downloading videos with audio requires ffmpeg (via ffmpeg.wasm) to be enabled to combine video and audio tracks. Therefore, this toggle lets you either use Redlib WebAssembly-free or utilize this feature. (videos will still play with audio)</div>
</details>
<input type="hidden" value="off" name="ffmpeg_video_downloads">
<input type="checkbox" name="ffmpeg_video_downloads" id="ffmpeg_video_downloads" {% if prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
</div> </div>
<div class="prefs-group"> <div class="prefs-group">
<label for="hide_hls_notification">Hide notification about possible HLS usage</label> <label for="hide_hls_notification">Hide notification about possible HLS usage</label>
@ -109,6 +137,10 @@
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>
</form> </form>
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div>
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs"> <div class="prefs" id="settings_subs">
<legend>Subscribed Feeds</legend> <legend>Subscribed Feeds</legend>
@ -139,11 +171,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&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 }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -64,25 +64,29 @@
{% call utils::post_in_list(post) %} {% call utils::post_in_list(post) %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if prefs.use_hls == "on" %} {% if prefs.ffmpeg_video_downloads == "on" %}
<script src="/ffmpeg/ffmpeg.js"></script>
<script src="/ffmpeg/ffmpeg-util.js"></script>
{% endif %}
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script> <script src="/videoUtils.js"></script>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<footer> <footer>
{% if !ends.0.is_empty() %} {% if !ends.0.is_empty() %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P"> PREV</a>
{% endif %} {% endif %}
{% if !ends.1.is_empty() %} {% if !ends.1.is_empty() %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT </a>
{% endif %} {% endif %}
</footer> </footer>
</div> </div>
{% endif %} {% endif %}
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %} {% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) && prefs.hide_sidebar_and_summary != "on" %}
<aside> <aside>
{% if is_filtered %} {% if is_filtered %}
<center>(Content from r/{{ sub.name }} has been filtered)</center> <center>(Content from r/{{ sub.name }} has been filtered)</center>
@ -133,7 +137,7 @@
</div> </div>
</div> </div>
</details> </details>
<details class="panel" id="sidebar"> <details class="panel" id="sidebar" open>
<summary id="sidebar_label">Sidebar</summary> <summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents"> <div id="sidebar_contents">
{{ sub.info|safe }} {{ sub.info|safe }}

View File

@ -71,9 +71,13 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if prefs.use_hls == "on" %} {% if prefs.ffmpeg_video_downloads == "on" %}
<script src="/ffmpeg/ffmpeg.js"></script>
<script src="/ffmpeg/ffmpeg-util.js"></script>
{% endif %}
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script> <script src="/videoUtils.js"></script>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@ -62,6 +62,7 @@
{%- endmacro %} {%- endmacro %}
{% macro post(post) -%} {% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT --> <!-- POST CONTENT -->
<div class="post highlighted"> <div class="post highlighted">
<p class="post_header"> <p class="post_header">
@ -93,6 +94,7 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a> style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} {% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h1> </h1>
<!-- POST MEDIA --> <!-- POST MEDIA -->
@ -101,12 +103,13 @@
<div class="post_media_content"> <div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image" > <a href="{{ post.media.url }}" class="post_media_image" >
{% if post.media.height == 0 || post.media.width == 0 %} {% if post.media.height == 0 || post.media.width == 0 %}
<!-- i.redd.it images speical case --> <!-- i.redd.it images special case -->
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"{%if post_should_be_blurred %} class="post_nsfw_blur"{% endif %}/>
{% else %} {% else %}
<svg <svg
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
{%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.media.url }}"/> <image width="100%" height="100%" href="{{ post.media.url }}"/>
<desc> <desc>
@ -117,18 +120,22 @@
</a> </a>
</div> </div>
{% else if post.post_type == "video" || post.post_type == "gif" %} {% else if post.post_type == "video" || post.post_type == "gif" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} {% if prefs.ffmpeg_video_downloads == "on" %}
<script src="/ffmpeg/ffmpeg.js"></script>
<script src="/ffmpeg/ffmpeg-util.js"></script>
{% endif %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls> <video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
<script src="/playHLSVideo.js"></script> <script src="/videoUtils.js"></script>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video{%if post_should_be_blurred %} post_nsfw_blur{% endif %}" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(post.permalink[1..]) %} {% call render_hls_notification(post.permalink[1..]) %}
{% endif %} {% endif %}
@ -194,6 +201,7 @@
{% endmacro %} {% endmacro %}
{% macro post_in_list(post) -%} {% macro post_in_list(post) -%}
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}"> <div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
<p class="post_header"> <p class="post_header">
{% let community -%} {% let community -%}
@ -222,7 +230,7 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a> dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} <a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h2> </h2>
<!-- POST MEDIA/THUMBNAIL --> <!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %} {% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
@ -233,7 +241,7 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/> <img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %} {% else %}
<svg <svg
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %} {%if post_should_be_blurred %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px" width="{{ post.media.width }}px"
height="{{ post.media.height }}px" height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -247,19 +255,19 @@
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none"> <video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short {%if post_should_be_blurred %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %} {% endif %}
@ -272,7 +280,7 @@
</svg> </svg>
{% else %} {% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;"> <div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg"> <svg {% if post_should_be_blurred %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/> <image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc> <desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/> <img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>