Compare commits

...

255 Commits

Author SHA1 Message Date
feedc572cd change bin name 2024-11-03 10:36:46 +13:00
a3bc16f7d2 port update checker to gitea compatible version 2024-11-03 10:08:26 +13:00
bd4cb96c0f "fix" scraper 2024-11-03 09:36:11 +13:00
a9c99cc752 Merge remote-tracking branch 'upstream/main' 2024-11-03 09:34:12 +13:00
Matthew Esposito
f03bdcf472
feat: display whether or not the instance is up to date on error (#310) 2024-11-01 18:16:25 -04:00
Matthew Esposito
2fd358f3ed
feat(hls): add video quality preference (#306) 2024-11-01 12:28:52 -04:00
Matthew Esposito
5ef57812f8 style: fix clippy 2024-11-01 11:39:05 -04:00
Nolan Poe
d17d097b12
Fix parts of CI (#304)
* Run cargo fmt, hide clippy::cmp_owned errors

* Bump deps

* Fix failing test

* Update src/client.rs

---------

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

* only have 32x32 .ico file

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

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

* fix apple-touch-icon.png size

* remove width, length

---------

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

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

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

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

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

* Make (most) emotes embed within comments

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

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

---------

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

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

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

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

Updated digest name on upload (as per 1187084)

* Update main-docker.yml (digests download)

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

* fix: set min size for video

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

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

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

* Remove the min-width requirement for the main column

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

* Make margins consistent between fixed and unfixed navbar settings

* Remove some empty space from deleted option

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

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

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

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

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

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

* Properly center search box instead of having it slightly skewed

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

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

* Make post flair line up with title

* Make post flair position consistent

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

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

* Fix extra navbar padding on search page

* Reduce gap between navbar and content in mobile mode

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 19:21:11 -04:00
b076b076e3 v0.35.2 2024-08-31 10:20:57 +12:00
fdaba8344d set video download MIME
fixes #11
2024-08-31 10:18:05 +12:00
Davide Cavalca
041ecceeaf
Update license tag (#200) 2024-08-07 14:09:54 -04:00
Butter Cat
3677ca10e2
Fix sort options overflow on small screens (#192) 2024-07-25 20:26:27 -04:00
Butter Cat
21fd34710c
Fix font sizes in search bar being incorrect (#190) 2024-07-24 19:56:06 -04:00
Jere Vuola
4205959ade
fix(docs): Add env HIDE_SIDEBAR_AND_SUMMARY (#188) 2024-07-23 21:02:15 -04:00
Matthew Esposito
27b56c1781
fix(client): handle new gated and quarantined error types (#187)
* fix(client): handle new gated and quarantined error types

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

* feat(rss): feature-ify rss

* feat(rss): config-ify rss

* fix(rss): update info page

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

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

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

* Update search widget semantic structure

* Make search bar design responsive on small screens

* Fix border color

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

* Improve user comment metadata design for mobile

* Remove formerly unused CSS class style

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

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

As such this style has been removed.
2024-07-17 21:27:13 -04:00
eedbe06af6 Merge branch 'ugly-layouts'
sure why not :)
- last words 2024
2024-07-07 19:34:27 +12:00
6042bca612 mosaic -> waterfall 2024-07-07 18:35:53 +12:00
7e85ae25b1 Merge remote-tracking branch 'upstream/main'
+ repository link update
2024-07-07 17:19:28 +12:00
Pim
4f21388643
fix: also use hls if possible for gifs in post_in_list macro (#177) 2024-07-05 16:33:06 -04:00
Pim
8a917fcde3
feat: add download button on image/gif/video posts (#173)
* feat: add download button on image/gif/video posts

* chore: fix formatting

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

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

* chore(clippy): fix lint
2024-06-28 22:28:58 -04:00
Matthew Esposito
102cd2f23f
Merge pull request #162 from redlib-org/oauth_arc_swap
fix(oauth): arc_swap
2024-06-28 18:17:00 -04:00
Matthew Esposito
3b2ad212d5 fix(oauth): arc_swap 2024-06-28 18:14:47 -04:00
Matthew Esposito
4dc7ff8165
Merge pull request #160 from redlib-org/oauth_oppenheimer
fix(oauth): even more atomics to avoid simultaneous token rollover
2024-06-27 23:35:51 -04:00
Matthew Esposito
2f8a38d8c7 chore(clippy): fix lint 2024-06-27 23:34:27 -04:00
Matthew Esposito
13083e999c fix(oauth): handle extremely rare race condition by atomically compare_exchanging 2024-06-27 23:32:17 -04:00
Matthew Esposito
4e2ec3fbc9 fix(oauth): handle case where a rate limit sneaks in 2024-06-27 23:29:55 -04:00
Matthew Esposito
89313f73e6 fix(oauth): atomics to avoid simultaneous token rollover 2024-06-27 23:26:31 -04:00
1644e68e43 v0.35.1 2024-06-28 13:34:12 +12:00
f5d4cc49fb fix some default options #7 2024-06-28 13:33:14 +12:00
f22f7841a8 Merge remote-tracking branch 'upstream/main' 2024-06-28 12:40:15 +12:00
b6c6e64cfe move the restore note
my scroll wheel will fall off if i have to scroll to get down there ohmagaw
2024-06-28 12:31:31 +12:00
6d3db31b11 Merge pull request 'README updates' (#9) from README-update into main
Reviewed-on: #9
2024-06-28 00:34:03 +12:00
f0eb496d6a fix README.md 2024-06-28 00:32:20 +12:00
e984646ae6 Update README.md
add information about the name, a thank you and bits here and there
2024-06-28 00:07:29 +12:00
60c0d63583 nsfw_landing style & function changes
theme the NSFW title and allow the user to directly change their setting
2024-06-27 17:43:43 +12:00
Matthew Esposito
3bd8b511a7 fix(oauth): strengthen sync guarantees 2024-06-26 23:41:26 -04:00
Matthew Esposito
8c5aaaa33d feat(scripts): add load testing 2024-06-26 23:40:31 -04:00
bd0a312487 add disclaimer 2024-06-27 15:04:58 +12:00
Matthew Esposito
023cc8505b
Merge pull request #158 from redlib-org/oauth_proper_atomics
fix(oauth): reset rate limit earlier in refresh cycle
2024-06-26 22:20:00 -04:00
Matthew Esposito
2e476dea63 fix(oauth): reset rate limit earlier in refresh cycle 2024-06-26 22:16:41 -04:00
f398d16c22 fix ffmpeg_video_downloads on cookie restore 2024-06-27 14:13:16 +12:00
Matthew Esposito
d045a5760a
Merge pull request #156 from redlib-org/fix_oauth_ratelimit
feat(oauth): roll over oauth key on rate limit
2024-06-26 19:33:04 -04:00
Matthew Esposito
07bf20dbc0 feat(oauth): roll over oauth key on rate limit 2024-06-26 19:19:30 -04:00
Matthew Esposito
518bf03e04 fix(client): Add trace logging for ratelimit info, render error page if exceeded 2024-06-26 08:05:22 -04:00
Matthew Esposito
00dee52320 chore(deps): Cargo update 2024-06-25 20:42:06 -04:00
Matthew Esposito
48873c01b9
Merge pull request #154 from redlib-org/fix_multi_sub
fix(subreddit): handle plus-encoding errors
2024-06-25 19:52:33 -04:00
Matthew Esposito
951fe400ae fix(subreddit): handle plus-encoding errors 2024-06-25 19:50:00 -04:00
Matthew Esposito
bacc9e35df refactor(oauth): leave android header unmodified (fixes #131) 2024-06-25 19:28:41 -04:00
Matthew Esposito
724b960112
Merge pull request #149 from pimlie/feat-blur-spoiler-previews
feat: add support to blur spoiler previews
2024-06-23 10:28:46 -04:00
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
pimlie
69f9d9ff3c feat: also blur spoiler previews on post view 2024-06-22 12:47:33 +02:00
pimlie
1d44bd180e feat: add spoiler badge to post title 2024-06-22 12:38:13 +02:00
pimlie
3301da1ef1 feat: add support to blur spoiler previews 2024-06-22 12:16:12 +02:00
138934f30c change resource links and container links 2024-06-21 23:49:37 +12:00
5396c0783e add mascots to user settings readme 2024-06-21 23:11:23 +12:00
Matthew Esposito
213481ef53
Merge pull request #148 from Pix3l01/fix-healthcheck
Make server listen on both IPv6 and IPv4 by default to fix docker healthcheck
2024-06-20 08:06:27 -04:00
Matthew Esposito
2d5cfcb41d
Merge pull request #146 from ac615223s5/default-filter
Default filters
2024-06-20 07:58:41 -04:00
Matthew Esposito
f460895cc0 chore(clippy): fix lint 2024-06-20 07:56:43 -04:00
ac615223s5
5193164719
Merge branch 'redlib-org:main' into default-filter 2024-06-19 20:10:05 -04:00
Matthew Esposito
9013e589dd chore(clippy): fix lint 2024-06-19 14:45:55 -04:00
Matthew Esposito
997cd8f829 feat(bug): Improve bug reporting while keeping logs private 2024-06-19 14:45:32 -04:00
Matthew Esposito
5a13b9892b chore(clippy): add lint 2024-06-19 14:28:48 -04:00
Alessandro Pizzorni
91975865b8 Make server listen on both IPv6 and IPv4 by default 2024-06-19 00:42:38 +02:00
nieve
3491e754ac Update .gitignore 2024-06-18 15:16:41 -04:00
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
nieve
1408c32a4d Update .env.example 2024-06-18 00:25:29 -04:00
nieve
30944579d7 add default filter config 2024-06-18 00:21:00 -04:00
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
Matthew Esposito
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
Sangeeth Sudheer
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
Matthew Esposito
a6f901c094
Merge pull request #128 from arulagrawal/nix-flake
add nix flake
2024-06-04 22:49:33 -04:00
Matthew Esposito
afad65f204
Merge pull request #127 from pimlie/patch-1
fix: healthcheck in Dockerfile
2024-06-04 22:48:51 -04:00
Matthew Esposito
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
Matthew Esposito
1132d73975
Merge pull request #134 from EMarshal/main 2024-06-02 22:08:38 -04:00
Éli Marshal
8ece7562ec Remove obsolete references to latest-arm and latest-armv7 images 2024-06-02 18:45:49 -06:00
Matthew Esposito
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
Arul Agrawal
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
Harm133
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
Ben Sherman
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
Matthew Esposito
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
Matthew Esposito
bd47c206a1 fix(oauth): Make Android user-agent patching unconditional 2024-05-30 18:08:45 -04:00
Matthew Esposito
7a099f259f v0.34.0 2024-05-29 20:59:27 -04:00
Matthew Esposito
4ea911e6b2 fix workflow again 2024-05-29 20:51:39 -04:00
Matthew Esposito
31d68afdc9 fix workflow 2024-05-29 20:50:38 -04:00
Matthew Esposito
96a7e155c5 v0.33.3 2024-05-29 20:49:29 -04:00
Matthew Esposito
045a8852ec fix workflow 2024-05-29 20:47:44 -04:00
Matthew Esposito
4cc8bf8318 v0.33.2 2024-05-29 20:05:56 -04:00
Matthew Esposito
1715b36ae9 Update fix for submitted route 2024-05-29 20:02:56 -04:00
Matthew Esposito
6102b08894 v0.33.1 2024-05-29 19:24:09 -04:00
Matthew Esposito
892b0e89c8 Cargo fmt 2024-05-29 19:15:54 -04:00
Matthew Esposito
a64e2143f3 Fix Display impl, resolving clippy 2024-05-29 18:51:20 -04:00
Matthew Esposito
e13d9b7239
Merge pull request #86 from EMarshal/main
Update hide HLS env var to match documentation
2024-05-29 18:49:40 -04:00
Matthew Esposito
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
Matthew Esposito
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
Matthew Esposito
26aa374bbd
Merge pull request #114 from ufUNnxagpM/main
feat: add icebergDark theme
2024-05-29 18:47:19 -04:00
Matthew Esposito
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
Matthew Esposito
093d240530
Merge branch 'main' into feat/hide-summary-and-sidebar 2024-05-29 18:46:10 -04:00
Matthew Esposito
9048565d48
Merge pull request #112 from Myzel394/fix-docs
Add `PORT` env to docs
2024-05-29 18:45:25 -04:00
Matthew Esposito
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
Matthew Esposito
6b11d936b3 Fix clippys 2024-05-29 18:44:19 -04:00
Butter Cat
34692359cf
Merge branch 'redlib-org:main' into fix_preview_captions 2024-05-29 18:42:39 -04:00
Matthew Esposito
33411a7588 Bump version 2024-05-29 18:40:09 -04:00
Matthew Esposito
5a0f0f96f5 Update OAuth resources 2024-05-29 18:37:13 -04:00
Matthew Esposito
273d889f1b Fix retrieval of multi-subs 2024-05-29 18:36:56 -04:00
backfire-monism-net
93594fc642
feat: add icebergDark theme 2024-05-24 22:16:49 -07:00
Myzel394
af6f3d3b3f
fix(docs): Add PORT env 2024-05-24 13:21:02 +02:00
Butter Cat
62b791bb24
Add in support for embedding i.redd.it images and gifs and remove leftover println 2024-05-23 21:29:36 -04:00
Butter Cat
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
Butter Cat
2f2cded671
Make sure new system can handle both normal and external previews 2024-05-22 17:22:10 -04:00
Butter Cat
b22fb7cd7b
Fix embedded preview images having a gap from the top of a comment 2024-05-22 16:40:57 -04:00
Butter Cat
75b0149313
Remove useless replace 2024-05-22 16:34:39 -04:00
Butter Cat
50fad938dd
Fix infinite loop when replacing text that contains dollar signs 2024-05-22 16:31:07 -04:00
Ales Lerch
b6f5831d10 feat: adds hide summary sidebar option 2024-05-13 23:49:59 +02:00
İsmail Karslı
565b50646f
added geo_filter query param to /r/popular endpoint 2024-04-25 14:01:06 +03:00
Butter Cat
6484ebf897
Fix failing check 2024-04-14 17:32:10 -04:00
Butter Cat
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
Butter Cat
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
Butter Cat
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
Butter Cat
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
Matthew Esposito
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
Butter Cat
89140c8cf7
Actually fix checks this time 2024-04-07 19:59:54 -04:00
Butter Cat
99048c4683
Fix failing checks 2024-04-07 19:51:20 -04:00
Butter Cat
ccfe7d0eeb
Make embedded images keep aspect ration when shrinking 2024-04-07 19:24:55 -04:00
Butter Cat
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
Butter Cat
9bd540d659
Stop post footer text from disappear at exactly 480px 2024-04-07 17:23:24 -04:00
Éli Marshal
4f6a14739b Update hide HLS env var to match documentation 2024-04-07 14:43:39 -06:00
Matthew Esposito
7c87d63d34
Merge pull request #85 from ButteredCats/embed_images
Embed Reddit preview links
2024-04-07 14:01:47 -04:00
Butter Cat
75b139dff2
Update image link test to account for embedded images 2024-04-07 12:08:53 -04:00
Butter Cat
10499df423
Make image preview links embed 2024-04-07 12:07:53 -04:00
Matthew Esposito
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
Butter Cat
858299c861
Add test for rewriting multiple preview links 2024-04-07 11:24:18 -04:00
Matthew Esposito
75f5c6668c
Merge pull request #77 from EMarshal/main
Update PUSHSHIFT_FRONTEND examples to undelete.pullpush.io
2024-04-07 10:53:40 -04:00
Butter Cat
e6b9a2e426
Fix anchor tags scrolling to the wrong place when fixed navbar is enabled (#82) 2024-04-07 10:52:59 -04:00
Daniel Nathan Gray
d4a2b3edc6
Add SVG logo (#84)
* Add SVG version of logo

* Add PNG generated from SVG
2024-04-07 10:51:33 -04:00
Butter Cat
4f0b29f930
Fix failing checks 2024-03-30 18:53:46 -04:00
Butter Cat
4e2648280d
Fix multiple Reddit preview links becoming the same 2024-03-30 18:36:28 -04:00
Éli Marshal
35ae71302f Update PUSHSHIFT_FRONTEND examples to undelete.pullpush.io
Matches the change in 3592728
2024-03-28 17:09:12 -06:00
77 changed files with 4450 additions and 2249 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target

View File

@ -9,11 +9,13 @@ REDLIB_BANNER=
# Disable search engine indexing
REDLIB_ROBOTS_DISABLE_INDEXING=off
# Set the Pushshift frontend for "removed" links
REDLIB_PUSHSHIFT_FRONTEND=www.unddit.com
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)
@ -24,20 +26,28 @@ REDLIB_DEFAULT_WIDE=off
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

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

5
.github/FUNDING.yml vendored
View File

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

View File

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

View File

@ -7,6 +7,8 @@ on:
- "compose.*"
branches:
- "main"
release:
types: [published]
env:
CARGO_TERM_COLOR: always
@ -60,7 +62,6 @@ jobs:
- name: Upload release
uses: softprops/action-gh-release@v1
if: github.base_ref != 'main' && github.event_name == 'release'
with:
tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}

View File

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

View File

@ -30,9 +30,15 @@ jobs:
with:
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
- 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
id: version
@ -45,17 +51,17 @@ jobs:
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- 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
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@v4
name: Upload a Build Artifact
with:
name: redlib
path: |
target/x86_64-unknown-linux-gnu/release/redlib
target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512
redlib.sha256
@ -68,7 +74,7 @@ jobs:
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
draft: true
files: |
target/x86_64-unknown-linux-gnu/release/redlib
target/x86_64-unknown-linux-musl/release/redlib
redlib.sha512
redlib.sha256
body: |

6
.gitignore vendored
View File

@ -1,4 +1,10 @@
/target
.env
redlib.toml
# Idea Files
.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>
accountForIssues <52367365+accountForIssues@users.noreply.github.com>
Adrian Lebioda <adrianlebioda@gmail.com>
Akanksh Chitimalla <55909985+Akanksh12@users.noreply.github.com>
alefvanoon <53198048+alefvanoon@users.noreply.github.com>
Ales Lerch <13370338+axeII@users.noreply.github.com>
Alexandre Iooss <erdnaxe@crans.org>
alyaeanyx <alexandra.hollmeier@mailbox.org>
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>
Arya K <73596856+gi-yt@users.noreply.github.com>
Austin Huang <im@austinhuang.me>
Ayaka <ayaka@kitty.community>
backfire-monism-net <development.0extl@simplelogin.com>
Basti <pred2k@users.noreply.github.com>
Ben Sherman <bennettmsherman@gmail.com>
Ben Smith <37027883+smithbm2316@users.noreply.github.com>
beucismis <beucismis@tutamail.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>
dacousb <53299044+dacousb@users.noreply.github.com>
Daniel Nathan Gray <dng@disroot.org>
Daniel Valentine <Daniel-Valentine@users.noreply.github.com>
Daniel Valentine <daniel@vielle.ws>
dbrennand <52419383+dbrennand@users.noreply.github.com>
Dean Sallinen <deza604@gmail.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com>
domve <domve@posteo.net>
Dyras <jevwmguf@duck.com>
Edward <101938856+EdwardLangdon@users.noreply.github.com>
Éli Marshal <835958+EMarshal@users.noreply.github.com>
elliot <75391956+ellieeet123@users.noreply.github.com>
erdnaxe <erdnaxe@users.noreply.github.com>
Esmail EL BoB <github.defilable@simplelogin.co>
fawn <fawn@envs.net>
FireMasterK <20838718+FireMasterK@users.noreply.github.com>
George Roubos <cowkingdom@hotmail.com>
git-bruh <e817509a-8ee9-4332-b0ad-3a6bdf9ab63f@aleeas.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>
Harsh Mishra <erbeusgriffincasper@gmail.com>
hinto.janai <hinto.janai@protonmail.com>
igna <igna@intent.cool>
imabritishcow <bcow@protonmail.com>
invakid404 <invakid404@riseup.net>
İsmail Karslı <ismail@karsli.net>
Johannes Schleifenbaum <johannes@js-webcoding.de>
Jonathan Dahan <git@jonathan.is>
Josiah <70736638+fres7h@users.noreply.github.com>
JPyke3 <pyke.jacob1@gmail.com>
Kavin <20838718+FireMasterK@users.noreply.github.com>
Kazi <kzshantonu@users.noreply.github.com>
Kieran <42723993+EnderDev@users.noreply.github.com>
Kieran <kieran@dothq.co>
Kirk1984 <christoph-m@posteo.de>
kuanhulio <66286575+kuanhulio@users.noreply.github.com>
Kyle Roth <kylrth@gmail.com>
laazyCmd <laazy.pr00gramming@protonmail.com>
Laurențiu Nicola <lnicola@users.noreply.github.com>
Lena <102762572+MarshDeer@users.noreply.github.com>
Leopardus <leopardus3@pm.me>
Macic <46872282+Macic-Dev@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 E <matt@matthew.science>
Matthew Esposito <matt@matthew.science>
Mennaruuk <52135169+Mennaruuk@users.noreply.github.com>
Midou36O <midou@midou.dev>
mikupls <93015331+mikupls@users.noreply.github.com>
Myzel394 <50424412+Myzel394@users.noreply.github.com>
Nainar <nainar.mb@gmail.com>
Nathan Moos <moosingin3space@gmail.com>
Nazar <63452145+Tokarak@users.noreply.github.com>
Nicholas Christopher <nchristopher@tuta.io>
Nick Lowery <ClockVapor@users.noreply.github.com>
Nico <github@dr460nf1r3.org>
NKIPSC <15067635+NKIPSC@users.noreply.github.com>
nohoster <136514837+nohoster@users.noreply.github.com>
o69mar <119129086+o69mar@users.noreply.github.com>
obeho <71698631+obeho@users.noreply.github.com>
obscurity <z@x4.pm>
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>
potatoesAreGod <118043038+potatoesAreGod@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>
The TwilightBlood <hwengerstickel@protonmail.com>
tirz <36501933+tirz@users.noreply.github.com>
tmak2002 <torben@tmak2002.dev>
Tokarak <63452145+Tokarak@users.noreply.github.com>
Tsvetomir Bonev <invakid404@riseup.net>
Vivek <vivek@revankar.net>
Vladislav Nepogodin <nepogodin.vlad@gmail.com>
Walkx <walkxnl@gmail.com>
Wichai <1482605+Chengings@users.noreply.github.com>
wsy2220 <wsy@dogben.com>
xatier <xatierlike@gmail.com>
Yaroslav Chvanov <yaroslav.chvanov@gmail.com>
Zach <72994911+zachjmurphy@users.noreply.github.com>

1106
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,30 @@
[package]
name = "redlib"
name = "redsunlib"
description = " Alternative private front-end to Reddit"
license = "AGPL-3.0"
repository = "https://github.com/redlib-org/redlib"
version = "0.31.2"
license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.3"
authors = [
"Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
]
edition = "2021"
default-run = "redsunlib"
[dependencies]
askama = { version = "0.12.1", default-features = false }
cached = { version = "0.48.1", features = ["async"] }
rinja = { version = "0.3.4", default-features = false }
cached = { version = "0.51.3", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [
"std",
"env",
"derive",
] }
regex = "1.10.2"
serde = { version = "1.0.193", features = ["derive"] }
cookie = "0.18.0"
futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] }
hyper-rustls = "0.25.0"
hyper-rustls = { version = "0.24.2", features = [ "http2" ] }
percent-encoding = "2.3.1"
route-recognizer = "0.3.1"
serde_json = "1.0.108"
@ -31,17 +33,22 @@ time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0"
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
libflate = "2.0.0"
brotli = { version = "3.4.0", features = ["std"] }
brotli = { version = "7.0.0", features = ["std"] }
toml = "0.8.8"
once_cell = "1.19.0"
serde_yaml = "0.9.29"
build_html = "2.4.0"
uuid = { version = "1.6.1", features = ["v4"] }
base64 = "0.21.5"
base64 = "0.22.1"
fastrand = "2.0.1"
log = "0.4.20"
pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1"
serde_json_path = "0.6.7"
async-recursion = "1.1.1"
[dev-dependencies]
lipsum = "0.9.0"
@ -51,3 +58,11 @@ sealed_test = "1.0.0"
codegen-units = 1
lto = true
strip = "symbols"
[[bin]]
name = "redsunlib"
path = "src/main.rs"
[[bin]]
name = "scraper"
path = "src/scraper/main.rs"

View File

@ -1,20 +1,34 @@
FROM alpine:3.19
## Builder
ARG TARGET
FROM rust:alpine AS builder
RUN apk add --no-cache curl
RUN apk add --no-cache musl-dev git
RUN curl -L https://github.com/redlib-org/redlib/releases/latest/download/redlib-${TARGET}.tar.gz | \
tar xz -C /usr/local/bin/
WORKDIR /redsunlib
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
USER redlib
COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --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 /redsunlib/target/x86_64-unknown-linux-musl/release/redsunlib /usr/local/bin/redsunlib
# Use an unprivileged user.
RUN adduser --home /nonexistent --no-create-home --disabled-password redsunlib
USER redsunlib
# 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"]
# 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
CMD ["redsunlib"]

174
README.md
View File

@ -1,46 +1,45 @@
# 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, with its origins in [Libreddit](https://github.com/libreddit/libreddit).
# 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.
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png)
<br>
---
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
**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://redlib.matthew.science/r/unpopularopinion) without being [tracked](#reddit).
### Disclaimer
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
- 🕵 Private: all requests are proxied through the server, including media
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
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. ❤️
---
## Table of Contents
1. [Redlib](#redlib)
2. [Instances](#instances)
3. [About](#about)
1. [Redsunlib](#redsunlib)
- [Disclaimer](#disclaimer)
2. [Table of Contents](#table-of-contents)
3. [Instances](#instances)
4. [About](#about)
- [The Name](#the-name)
- [Built with](#built-with)
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
- [Teddit](#teddit)
- [Libreddit](#libreddit)
4. [Comparison](#comparison)
5. [Comparison](#comparison)
- [Speed](#speed)
- [Privacy](#privacy)
- [Reddit](#reddit)
- [Redlib](#redlib-1)
- [Server](#server)
- [Official instance (redlib.matthew.science)](#official-instance-redlibmatthewscience)
5. [Deployment](#deployment)
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)
- [Replit/Heroku/Glitch](#replit-heroku-glitch)
- [launchd (macOS)](#launchd-macos)
6. [Configuration](#configuration)
7. [Configuration](#configuration)
- [Instance settings](#instance-settings)
- [Default user settings](#default-user-settings)
@ -48,31 +47,30 @@
# Instances
> [!TIP]
> 🔗 **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**
An up-to-date table of instances is available in [Markdown](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) and [machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json).
Both files are part of the [redlib-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [redlib-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
For information on instance uptime, see the [Uptime Robot status page](https://stats.uptimerobot.com/mpmqAs1G2Q).
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." ;)
---
# About
> [!NOTE]
> 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
<sup>I do self criticism constantly, because I'm trapped in a Maoist *cult* where comrades (white terrorists) criticize me merciloussly for having a fascist credit card (VISA Silver Signature Rewards) They won't let me order vegan pizza anymore because the phone is fascist and "summoning my pizza slave with bourgeois app" is "bad vibes"</sup>
## Built with
- [Rust](https://www.rust-lang.org/) - Programming language
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
- [Askama](https://github.com/djc/askama) - Templating engine
- [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
- [Rustls](https://github.com/rustls/rustls) - TLS library
## How is it different from other Reddit front ends?
@ -158,16 +156,6 @@ For transparency, I hope to describe all the ways Redlib handles user privacy.
- **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 (redlib.matthew.science)
The official instance is hosted at https://redlib.matthew.science.
- **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.
---
# Deployment
@ -180,7 +168,7 @@ For configuration options, see the [Configuration section](#Configuration).
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
Docker images for Redlib are available at [quay.io](https://quay.io/repository/redlib/redlib), with support for `amd64`, `arm64`, and `armv7` platforms.
Docker images for Redsunlib are available at our [Gitea container registry](https://git.stardust.wtf/iridium/-/packages/container/redsunlib/latest), currently only with support for `amd64`, if you need `arm64`, or `armv7` platforms you can either build Redsunlib yourself or open an [issue](https://git.stardust.wtf/iridium/redsunlib/issues) :)
### Docker Compose
@ -203,24 +191,18 @@ docker logs -f redlib
### Docker CLI
> [!IMPORTANT]
> If deploying on:
>
> - an `arm64` platform, use the `quay.io/redlib/redlib:latest-arm` image instead.
> - an `armv7` platform, use the `quay.io/redlib/redlib:latest-armv7` image instead.
Deploy Redlib:
```bash
docker pull quay.io/redlib/redlib:latest
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib:latest
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 quay.io/redlib/redlib:latest
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib:latest
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`.
@ -233,19 +215,7 @@ docker logs -f redlib
## Binary
If you're on Linux, you can grab a binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest) from GitHub.
Download the binary using [Wget](https://www.gnu.org/software/wget/):
```bash
wget https://github.com/redlib-org/redlib/releases/download/v0.31.0/redlib
```
Make the binary executable and change its ownership to `root`:
```bash
sudo chmod +x redlib && sudo chown root:root redlib
```
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`:
@ -289,59 +259,13 @@ Before=nginx.service
## Building from source
To deploy Redlib with changes not yet included in the latest release, you can build the application from source.
To deploy Redsunlib with changes not yet included in the latest release, you can build the application from source.
```bash
git clone https://github.com/redlib-org/redlib && cd redlib
git clone https://git.stardust.wtf/iridium/redsunlib && cd redsunlib
cargo run
```
## Replit/Heroku
> [!WARNING]
> These are free hosting options, but they are _not_ private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
## launchd (macOS)
If you are on macOS, you can use the [launchd](https://en.wikipedia.org/wiki/Launchd) service available in `contrib/redlib.plist`.
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`.
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
<!-- ## Cargo
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
```bash
cargo install libreddit
``` -->
<!-- ## AUR
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
```bash
yay -S libreddit-git
```
## NetBSD/pkgsrc
For NetBSD users, Redlib is available from the official repositories.
```bash
pkgin install libreddit
```
Or, if you prefer to build from source
```bash
cd /usr/pkgsrc/libreddit
make install
``` -->
---
# Configuration
@ -371,7 +295,7 @@ REDLIB_DEFAULT_USE_HLS = "on"
> 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 quay.io/redlib/redlib:latest
> docker run -d --name redlib -p 8080:8080 --env-file .env git.stardust.wtf/iridium/redsunlib:latest
> ```
>
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
@ -380,32 +304,38 @@ REDLIB_DEFAULT_USE_HLS = "on"
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. |
| 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. |
| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
## Default user settings
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `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` |
| `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |

View File

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

View File

@ -2,25 +2,25 @@
version: "3.8"
services:
redlib:
redsunlib:
build: .
restart: always
container_name: "redlib"
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-redlib.json
# - seccomp=seccomp-redsunlib.json
cap_drop:
- ALL
networks:
- redlib
- redsunlib
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
interval: 5m
timeout: 3s
networks:
redlib:
redsunlib:

View File

@ -1,26 +1,24 @@
services:
redlib:
image: quay.io/redlib/redlib:latest
# image: quay.io/redlib/redlib:latest-arm # uncomment if you use arm64
# image: quay.io/redlib/redlib:latest-armv7 # uncomment if you use armv7
redsunlib:
image: git.stardust.wtf/iridium/redsunlib:latest
restart: always
container_name: "redlib"
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-redlib.json
# - seccomp=seccomp-redsunlib.json
cap_drop:
- ALL
env_file: .env
networks:
- redlib
- redsunlib
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
interval: 5m
timeout: 3s
networks:
redlib:
redsunlib:

View File

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

View File

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

106
flake.lock 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

View File

@ -1,17 +1,20 @@
use arc_swap::ArcSwap;
use cached::proc_macro::cached;
use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper::header::HeaderValue;
use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector;
use libflate::gzip;
use log::error;
use log::{error, trace, warn};
use once_cell::sync::Lazy;
use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicBool, AtomicU16};
use std::{io, result::Result};
use tokio::sync::RwLock;
use crate::dbg_msg;
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
@ -19,23 +22,34 @@ use crate::server::RequestExt;
use crate::utils::format_url;
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.expect("No native root certificates found")
.https_only()
.enable_http1()
.build();
client::Client::builder().build(https)
});
const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new());
tokio::spawn(token_daemon());
RwLock::new(client)
ArcSwap::new(client.into())
});
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
static URL_PAIRS: [(&str, &str); 2] = [
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
];
/// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`.
///
@ -49,13 +63,32 @@ pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
/// `Location` header. An `Err(String)` is returned if Reddit responds with a
/// 429, or if we were unable to decode the value in the `Location` header.
#[cached(size = 1024, time = 600, result = true)]
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
let res = reddit_head(path.clone(), true).await?;
#[async_recursion::async_recursion]
pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
if tries == 0 {
return Ok(None);
}
// for each URL pair, try the HEAD request
let res = {
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
let mut res = None;
for (url_base, url_base_host) in URL_PAIRS {
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
if let Some(res) = &res {
if !res.status().is_client_error() {
break;
}
}
}
res
};
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
let status = res.status().as_u16();
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
match status {
429 => Err("Too many requests.".to_string()),
// If Reddit responds with a 2xx, then the path is already canonical.
200..=299 => Ok(Some(path)),
@ -65,6 +98,7 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
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.
// In addition, we want to remove share parameters.
// Cut it off here instead of letting it propagate all the way
@ -77,7 +111,9 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// also remove all Reddit domain parts with format_url.
// Otherwise, it will literally redirect to Reddit.com.
let uri = format_url(stripped_uri);
Ok(Some(uri))
// Decrement tries and try again
canonical_path(uri, tries - 1).await
}
None => Ok(None),
},
@ -86,6 +122,12 @@ pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
// as above), return a None.
300..=399 => Ok(None),
// Rate limiting
429 => Err("Too many requests.".to_string()),
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
403 if policy_error => Err("Too many requests.".to_string()),
_ => Ok(
res
.headers()
@ -112,7 +154,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Build the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone();
let client: &Lazy<Client<_, Body>> = &CLIENT;
let mut builder = Request::get(parsed_uri);
@ -152,26 +194,32 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
/// 3xx codes Reddit returns and will automatically redirect.
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
request(&Method::GET, path, true, quarantine)
request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
}
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
request(&Method::HEAD, path, false, quarantine)
/// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
request(&Method::HEAD, path, false, quarantine, base_path, host)
}
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
// request(&Method::HEAD, path, false, quarantine, false)
// }
// Unused - reddit_head is only ever called in the context of a short URL
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
/// will recurse on the URL that Reddit provides in the Location HTTP header
/// in its response.
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
// Build Reddit URL from path.
let url = format!("{REDDIT_URL_BASE}{path}");
let url = format!("{base_path}{path}");
// Construct the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone();
let client: &Lazy<Client<_, Body>> = &CLIENT;
let (token, vendor_id, device_id, user_agent, loid) = {
let client = block_on(OAUTH_CLIENT.read());
let client = OAUTH_CLIENT.load_full();
(
client.token.clone(),
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
@ -180,6 +228,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
)
};
// Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.)
let builder = Request::builder()
@ -189,7 +238,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.header("Client-Vendor-Id", vendor_id)
.header("X-Reddit-Device-Id", device_id)
.header("x-reddit-loid", loid)
.header("Host", "oauth.reddit.com")
.header("Host", host)
.header("Authorization", &format!("Bearer {token}"))
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5")
@ -214,12 +263,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
if !redirect {
return Ok(response);
};
let location_header = response.headers().get(header::LOCATION);
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
return Err("Reddit response was invalid".to_string());
}
return request(
method,
response
.headers()
.get(header::LOCATION)
location_header
.map(|val| {
// We need to make adjustments to the URI
// we get back from Reddit. Namely, we
@ -232,13 +282,19 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// required.
//
// 2. Percent-encode the path.
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string();
let new_path = percent_encode(val.as_bytes(), CONTROLS)
.to_string()
.trim_start_matches(REDDIT_URL_BASE)
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
.to_string();
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
})
.unwrap_or_default()
.to_string(),
true,
quarantine,
base_path,
host,
)
.await;
};
@ -291,7 +347,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
}
}
Err(e) => {
dbg_msg!("{} {}: {}", method, path, e);
dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
Err(e.to_string())
}
@ -306,23 +362,70 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
#[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// 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);
Err(format!("{msg}: {e}"))
Err(format!("{msg}: {e} | {path}"))
};
// First, handle rolling over the OAUTH_CLIENT if need be.
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
let is_rolling_over = OAUTH_IS_ROLLING_OVER.load(Ordering::SeqCst);
if current_rate_limit < 10 && !is_rolling_over {
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
tokio::spawn(force_refresh_token());
}
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
// Fetch the url...
match reddit_get(path.clone(), quarantine).await {
Ok(response) => {
let status = response.status();
let reset: Option<String> = if let (Some(remaining), Some(reset), Some(used)) = (
response.headers().get("x-ratelimit-remaining").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
) {
trace!(
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
if is_rolling_over { "yes" } else { "no" },
);
Some(reset)
} else {
None
};
// asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await {
Ok(body) => {
let has_remaining = body.has_remaining();
if !has_remaining {
// Rate limited, so spawn a force_refresh_token()
tokio::spawn(force_refresh_token());
return match reset {
Some(val) => Err(format!(
"Reddit rate limit exceeded. Try refreshing in a few seconds.\
Rate limit will reset in: {val}"
)),
None => Err("Reddit rate limit exceeded".to_string()),
};
}
// Parse the response from Reddit as JSON
match serde_json::from_reader(body.reader()) {
Ok(value) => {
let json: Value = value;
// If user is suspended
if let Some(data) = json.get("data") {
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
if is_suspended {
return Err("suspended".into());
}
}
}
// If Reddit returned an error
if json["error"].is_i64() {
// OAuth token has expired; http status 401
@ -331,7 +434,25 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
let () = force_refresh_token().await;
return Err("OAuth token has expired. Please refresh the page!".to_string());
}
Err(format!("Reddit error {} \"{}\": {}", json["error"], json["reason"], json["message"]))
// Handle quarantined
if json["reason"] == "quarantined" {
return Err("quarantined".into());
}
// Handle gated
if json["reason"] == "gated" {
return Err("gated".into());
}
// Handle private subs
if json["reason"] == "private" {
return Err("private".into());
}
// Handle banned subs
if json["reason"] == "banned" {
return Err("banned".into());
}
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
} else {
Ok(json)
}
@ -341,21 +462,24 @@ pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string())
} 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")]
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());
}
@ -363,13 +487,34 @@ async fn test_localization_popular() {
async fn test_obfuscated_share_link() {
let share_link = "/r/rust/s/kPgq8WNHRK".into();
// Correct link without share parameters
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into();
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link)));
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_share_link_strip_json() {
let link = "/17krzvz".into();
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into();
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link)));
let canonical_link = "/comments/17krzvz".into();
assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_private_sub() {
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("private".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_banned_sub() {
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
assert!(link.is_err());
assert_eq!(link, Err("banned".into()));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_gated_sub() {
// quarantine to false to specifically catch when we _don't_ catch it
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
assert!(link.is_err());
assert_eq!(link, Err("gated".into()));
}

View File

@ -28,6 +28,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_THEME")]
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(alias = "LIBREDDIT_DEFAULT_FRONT_PAGE")]
pub(crate) default_front_page: Option<String>,
@ -48,6 +52,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
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(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
pub(crate) default_show_nsfw: Option<String>,
@ -60,6 +68,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")]
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(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
pub(crate) default_hide_hls_notification: Option<String>,
@ -68,6 +80,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
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(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
pub(crate) default_hide_score: Option<String>,
@ -76,6 +92,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
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(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -91,6 +111,12 @@ pub struct Config {
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>,
#[serde(rename = "REDLIB_ENABLE_RSS")]
pub(crate) enable_rss: Option<String>,
#[serde(rename = "REDLIB_FULL_URL")]
pub(crate) full_url: Option<String>,
}
impl Config {
@ -117,22 +143,29 @@ impl Config {
Self {
sfw_only: parse("REDLIB_SFW_ONLY"),
default_theme: parse("REDLIB_DEFAULT_THEME"),
default_mascot: parse("REDLIB_DEFAULT_MASCOT"),
default_front_page: parse("REDLIB_DEFAULT_FRONT_PAGE"),
default_layout: parse("REDLIB_DEFAULT_LAYOUT"),
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
default_wide: parse("REDLIB_DEFAULT_WIDE"),
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_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
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_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
}
}
}
@ -141,22 +174,29 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
match name {
"REDLIB_SFW_ONLY" => config.sfw_only.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_LAYOUT" => config.default_layout.clone(),
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_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_BLUR_NSFW" => config.default_blur_nsfw.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_WIDE" => config.default_wide.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_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_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
"REDLIB_FULL_URL" => config.full_url.clone(),
_ => None,
}
}
@ -225,6 +265,12 @@ fn test_default_subscriptions() {
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]
#[sealed_test]
fn test_pushshift() {

View File

@ -5,8 +5,8 @@ use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
use askama::Template;
use hyper::{Body, Request, Response};
use rinja::Template;
use serde_json::Value;
use std::borrow::ToOwned;
use std::collections::HashSet;
@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
}
if have_after {
before = "t3_".to_owned();
"t3_".clone_into(&mut before);
before.push_str(&duplicates[0].id);
}
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
if have_before {
// The next batch will need to start from one after the
// last post in the current batch.
after = "t3_".to_owned();
"t3_".clone_into(&mut after);
after.push_str(&duplicates[l - 1].id);
// Here is where things get terrible. Notice that we
@ -182,7 +182,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
match json(new_path, true).await {
Ok(response) => {
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);
}
}

View File

@ -3,10 +3,10 @@ use crate::{
server::RequestExt,
utils::{ErrorTemplate, Preferences},
};
use askama::Template;
use build_html::{Container, Html, HtmlContainer, Table};
use hyper::{http::Error, Body, Request, Response};
use once_cell::sync::Lazy;
use rinja::Template;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
@ -85,7 +85,7 @@ fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
pub struct InstanceInfo {
package_name: String,
crate_version: String,
git_commit: String,
pub git_commit: String,
deploy_date: String,
compile_mode: String,
deploy_unix_ts: i64,
@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)],
["RSS enabled", &convert(&self.config.enable_rss)],
["Full URL", &convert(&self.config.full_url)],
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
])
.with_header_row(["Settings"]),
@ -136,16 +138,20 @@ impl InstanceInfo {
["Hide awards", &convert(&self.config.default_hide_awards)],
["Hide score", &convert(&self.config.default_hide_score)],
["Theme", &convert(&self.config.default_theme)],
["Mascot", &convert(&self.config.default_mascot)],
["Front page", &convert(&self.config.default_front_page)],
["Layout", &convert(&self.config.default_layout)],
["Wide", &convert(&self.config.default_wide)],
["Comment sort", &convert(&self.config.default_comment_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)],
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
["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)],
["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)],
])
.with_header_row(["Default preferences"]),
);
@ -163,21 +169,27 @@ impl InstanceInfo {
Compile mode: {}\n
SFW only: {:?}\n
Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n
Banner: {:?}\n
Hide awards: {:?}\n
Hide score: {:?}\n
Default theme: {:?}\n
Default mascot: {:?}\n
Default front page: {:?}\n
Default layout: {:?}\n
Default wide: {:?}\n
Default comment sort: {:?}\n
Default post sort: {:?}\n
Default blur Spoiler: {:?}\n
Default show NSFW: {:?}\n
Default blur NSFW: {:?}\n
Default use HLS: {:?}\n
Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n",
Default subscriptions: {:?}\n
Default filters: {:?}\n",
self.package_name,
self.crate_version,
self.git_commit,
@ -185,21 +197,27 @@ impl InstanceInfo {
self.deploy_unix_ts,
self.compile_mode,
self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift,
self.config.banner,
self.config.default_hide_awards,
self.config.default_hide_score,
self.config.default_theme,
self.config.default_mascot,
self.config.default_front_page,
self.config.default_layout,
self.config.default_wide,
self.config.default_comment_sort,
self.config.default_post_sort,
self.config.default_blur_spoiler,
self.config.default_show_nsfw,
self.config.default_blur_nsfw,
self.config.default_use_hls,
self.config.default_ffmpeg_video_downloads,
self.config.default_hide_hls_notification,
self.config.default_subscriptions,
self.config.default_filters,
)
}
StringType::Html => self.to_table(),

13
src/lib.rs Normal file
View File

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

View File

@ -2,35 +2,21 @@
#![forbid(unsafe_code)]
#![allow(clippy::cmp_owned)]
// Reference local files
mod config;
mod duplicates;
mod instance_info;
mod oauth;
mod oauth_resources;
mod post;
mod search;
mod settings;
mod subreddit;
mod user;
mod utils;
// Import Crates
use cached::proc_macro::cached;
use clap::{Arg, ArgAction, Command};
use std::str::FromStr;
use futures_lite::FutureExt;
use hyper::Uri;
use hyper::{header::HeaderValue, Body, Request, Response};
mod client;
use client::{canonical_path, proxy};
use log::info;
use once_cell::sync::Lazy;
use server::RequestExt;
use utils::{error, redirect, ThemeAssets};
use redsunlib::client::{canonical_path, proxy, CLIENT};
use redsunlib::server::{self, RequestExt};
use redsunlib::utils::{error, redirect, ThemeAssets, MascotAssets};
use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user};
use crate::client::OAUTH_CLIENT;
mod server;
use redsunlib::client::OAUTH_CLIENT;
// Create Services
@ -78,6 +64,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> {
let mut res = Response::builder()
.status(200)
@ -111,6 +108,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]
async fn main() {
// Load environment variables
@ -135,7 +146,7 @@ async fn main() {
.long("address")
.value_name("ADDRESS")
.help("Sets address to listen on")
.default_value("0.0.0.0")
.default_value("[::]")
.num_args(1),
)
.arg(
@ -166,7 +177,7 @@ async fn main() {
let listener = [address, ":", port].concat();
println!("Starting Redlib...");
println!("Starting Redsunlib...");
// Begin constructing a server
let mut app = server::Server::new();
@ -189,7 +200,7 @@ async fn main() {
"Referrer-Policy" => "no-referrer",
"X-Content-Type-Options" => "nosniff",
"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 {
@ -224,14 +235,40 @@ async fn main() {
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
app
.at("/playHLSVideo.js")
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
.at("/videoUtils.js")
.get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed());
app
.at("/hls.min.js")
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
app
.at("/highlighted.js")
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
app
.at("/check_update.js")
.get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed());
app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed());
// 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
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
@ -239,6 +276,9 @@ async fn main() {
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
app
.at("/emote/:subreddit_id/:filename")
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
app
.at("/preview/:loc/award_images/:fullname/:id")
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
@ -254,6 +294,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -265,6 +306,12 @@ async fn main() {
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed());
// Mascots
app.at("/mascot/:name").get(|r| mascot_image(r).boxed());
// RSS Subscriptions
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());
// Subreddit services
app
.at("/r/:sub")
@ -337,7 +384,7 @@ async fn main() {
let sub = req.param("sub").unwrap_or_default();
match req.param("id").as_deref() {
// Share link
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}")).await {
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
Ok(Some(path)) => Ok(redirect(&path)),
Ok(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,
@ -356,7 +403,7 @@ async fn main() {
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
// Short link for post
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}")).await {
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
Ok(path_opt) => match path_opt {
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,
@ -373,7 +420,7 @@ async fn main() {
// Default service in case no routes match
app.at("/*").get(|req| error(req, "Nothing here").boxed());
println!("Running Redlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
println!("Running Redsunlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
let server = app.listen(&listener);
@ -382,3 +429,22 @@ async fn main() {
eprintln!("Server error: {e}");
}
}
pub async fn proxy_commit_info() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/atom+xml")
.body(Body::from(fetch_commit_info().await))
.unwrap_or_default(),
)
}
#[cached(time = 600)]
async fn fetch_commit_info() -> String {
let uri = Uri::from_str("https://git.stardust.wtf/api/v1/repos/iridium/redsunlib/commits?verification=false&stat=false").expect("Invalid URI");
let resp: Body = CLIENT.get(uri).await.expect("Failed to request git.stardust.wtf").into_body();
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
}

View File

@ -1,18 +1,19 @@
use std::{collections::HashMap, time::Duration};
use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
use crate::{
client::{CLIENT, OAUTH_CLIENT},
client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
oauth_resources::ANDROID_APP_VERSION_LIST,
};
use base64::{engine::general_purpose, Engine as _};
use hyper::{client, Body, Method, Request};
use log::info;
use log::{error, info, trace};
use serde_json::json;
use tokio::time::{error::Elapsed, timeout};
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
static AUTH_ENDPOINT: &str = "https://accounts.reddit.com";
static AUTH_ENDPOINT: &str = "https://www.reddit.com";
// Spoofed client for Android devices
#[derive(Debug, Clone, Default)]
@ -25,11 +26,32 @@ pub struct Oauth {
}
impl Oauth {
/// Create a new OAuth client
pub(crate) async fn new() -> Self {
let mut oauth = Self::default();
oauth.login().await;
oauth
// Call new_internal until it succeeds
loop {
let attempt = Self::new_with_timeout().await;
match attempt {
Ok(Some(oauth)) => {
info!("[✅] Successfully created OAuth client");
return oauth;
}
Ok(None) => {
error!("Failed to create OAuth client. Retrying in 5 seconds...");
continue;
}
Err(duration) => {
error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds...");
}
}
}
}
async fn new_with_timeout() -> Result<Option<Self>, Elapsed> {
let mut oauth = Self::default();
timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth))
}
pub(crate) fn default() -> Self {
// Generate a device to spoof
let device = Device::new();
@ -46,7 +68,7 @@ impl Oauth {
}
async fn login(&mut self) -> Option<()> {
// Construct URL for OAuth token
let url = format!("{AUTH_ENDPOINT}/api/access_token");
let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid");
let mut builder = Request::builder().method(Method::POST).uri(&url);
// Add headers from spoofed client
@ -69,13 +91,19 @@ impl Oauth {
// Build request
let request = builder.body(body).unwrap();
trace!("Sending token request...");
// Send request
let client: client::Client<_, Body> = CLIENT.clone();
let client: &once_cell::sync::Lazy<client::Client<_, Body>> = &CLIENT;
let resp = client.request(request).await.ok()?;
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
// Technically it's not needed, but it's easy for Reddit API to check for this.
// It's some kind of header that uniquely identifies the device.
// Not worried about the privacy implications, since this is randomly changed
// and really only as privacy-concerning as the OAuth token itself.
if let Some(header) = resp.headers().get("x-reddit-loid") {
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
}
@ -85,10 +113,14 @@ impl Oauth {
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
}
trace!("Serializing response...");
// Serialize response
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
trace!("Accessing relevant fields...");
// Save token and expiry
self.token = json.get("access_token")?.as_str()?.to_string();
self.expires_in = json.get("expires_in")?.as_u64()?;
@ -98,21 +130,13 @@ impl Oauth {
Some(())
}
async fn refresh(&mut self) -> Option<()> {
// Refresh is actually just a subsequent login with the same headers (without the old token
// or anything). This logic is handled in login, so we just call login again.
let refresh = self.login().await;
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
refresh
}
}
pub async fn token_daemon() {
// Monitor for refreshing token
loop {
// Get expiry time - be sure to not hold the read lock
let expires_in = { OAUTH_CLIENT.read().await.expires_in };
let expires_in = { OAUTH_CLIENT.load_full().expires_in };
// sleep for the expiry time minus 2 minutes
let duration = Duration::from_secs(expires_in - 120);
@ -125,13 +149,22 @@ pub async fn token_daemon() {
// Refresh token - in its own scope
{
OAUTH_CLIENT.write().await.refresh().await;
force_refresh_token().await;
}
}
}
pub async fn force_refresh_token() {
OAUTH_CLIENT.write().await.refresh().await;
if OAUTH_IS_ROLLING_OVER.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
trace!("Skipping refresh token roll over, already in progress");
return;
}
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
let new_client = Oauth::new().await;
OAUTH_CLIENT.swap(new_client.into());
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
OAUTH_IS_ROLLING_OVER.store(false, Ordering::SeqCst);
}
#[derive(Debug, Clone, Default)]
@ -179,21 +212,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_client_refresh() {
OAUTH_CLIENT.write().await.refresh().await.unwrap();
force_refresh_token().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_token_exists() {
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_oauth_headers_len() {
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3);
assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
}
#[test]

View File

@ -4,6 +4,44 @@
// Filled in with real app versions
pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
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.26.0/Build 1019073",
"Version 2023.27.0/Build 1031923",
"Version 2023.28.0/Build 1046887",
"Version 2023.29.0/Build 1059855",
"Version 2023.30.0/Build 1078734",
@ -26,14 +64,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.44.0/Build 1268622",
"Version 2023.45.0/Build 1281371",
"Version 2023.47.0/Build 1303604",
"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 2022.42.0/Build 638508",
"Version 2022.43.0/Build 648277",
"Version 2022.44.0/Build 664348",
"Version 2022.45.0/Build 677985",
"Version 2023.01.0/Build 709875",
"Version 2023.02.0/Build 717912",
"Version 2023.03.0/Build 729220",
"Version 2023.04.0/Build 744681",
"Version 2023.05.0/Build 755453",
"Version 2023.06.0/Build 775017",
"Version 2023.07.0/Build 788827",
@ -56,14 +94,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2023.19.0/Build 927681",
"Version 2023.20.0/Build 943980",
"Version 2023.20.1/Build 946732",
"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.26.0/Build 1019073",
"Version 2023.27.0/Build 1031923",
"Version 2022.20.0/Build 487703",
"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.24.0/Build 510950",
"Version 2022.24.1/Build 513462",
"Version 2022.25.0/Build 515072",
"Version 2022.25.1/Build 516394",
"Version 2022.25.2/Build 519915",
"Version 2022.26.0/Build 521193",
@ -86,14 +124,14 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.40.0/Build 624782",
"Version 2022.41.0/Build 630468",
"Version 2022.41.1/Build 634168",
"Version 2022.42.0/Build 638508",
"Version 2022.43.0/Build 648277",
"Version 2022.44.0/Build 664348",
"Version 2022.45.0/Build 677985",
"Version 2023.01.0/Build 709875",
"Version 2023.02.0/Build 717912",
"Version 2023.03.0/Build 729220",
"Version 2023.04.0/Build 744681",
"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",
"Version 2021.45.0/Build 387663",
"Version 2021.46.0/Build 392043",
"Version 2021.47.0/Build 394342",
"Version 2022.10.0/Build 429896",
"Version 2022.1.0/Build 402829",
"Version 2022.11.0/Build 433004",
@ -106,15 +144,7 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.17.0/Build 468480",
"Version 2022.18.0/Build 473740",
"Version 2022.19.1/Build 482464",
"Version 2022.20.0/Build 487703",
"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.24.0/Build 510950",
"Version 2022.24.1/Build 513462",
"Version 2022.25.0/Build 515072",
"Version 2022.3.0/Build 408637",
"Version 2022.4.0/Build 411368",
"Version 2022.5.0/Build 414731",
@ -124,35 +154,5 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
"Version 2022.7.0/Build 420849",
"Version 2022.8.0/Build 423906",
"Version 2022.9.0/Build 426592",
"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",
"Version 2021.45.0/Build 387663",
"Version 2021.46.0/Build 392043",
"Version 2021.47.0/Build 394342",
];
pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""];

View File

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

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

@ -0,0 +1,75 @@
use std::{fmt::Display, io::Write};
use clap::{Parser, ValueEnum};
use redsunlib::utils::Post;
#[derive(Parser)]
#[command(name = "my_cli")]
#[command(about = "A simple CLI example", long_about = None)]
struct Cli {
#[arg(short = 's', long = "sub")]
sub: String,
#[arg(short = 'c', long = "count")]
count: usize,
#[arg(long = "sort")]
sort: SortOrder,
#[arg(short = 'f', long = "format", value_enum)]
format: Format,
#[arg(short = 'o', long = "output")]
output: Option<String>,
}
#[derive(Debug, Clone, ValueEnum)]
enum SortOrder {
Hot,
Rising,
New,
Top,
Controversial,
}
impl Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortOrder::Hot => write!(f, "hot"),
SortOrder::Rising => write!(f, "rising"),
SortOrder::New => write!(f, "new"),
SortOrder::Top => write!(f, "top"),
SortOrder::Controversial => write!(f, "controversial"),
}
}
}
#[derive(Debug, Clone, ValueEnum)]
enum Format {
Json,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let (sub, final_count, sort, format, output) = (cli.sub, cli.count, cli.sort, cli.format, cli.output);
let initial = format!("/r/{sub}/{sort}.json?&raw_json=1");
let (mut posts, mut after) = Post::fetch(&initial, false).await.unwrap();
while posts.len() < final_count {
print!("\r");
let path = format!("/r/{sub}/{sort}.json?sort={sort}&t=&after={after}&raw_json=1");
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
posts.extend(new_posts);
after = new_after;
// Print number of posts fetched
print!("Fetched {} posts", posts.len());
std::io::stdout().flush().unwrap();
}
match format {
Format::Json => {
let filename: String = output.unwrap_or_else(|| format!("{sub}.json"));
let json = serde_json::to_string(&posts).unwrap();
std::fs::write(filename, json).unwrap();
}
}
}

View File

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

View File

@ -1,3 +1,6 @@
#![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
use cached::proc_macro::cached;
use cookie::Cookie;
@ -15,6 +18,7 @@ use libflate::gzip;
use route_recognizer::{Params, Router};
use std::{
cmp::Ordering,
fmt::Display,
io,
pin::Pin,
result::Result,
@ -65,12 +69,12 @@ impl CompressionType {
}
}
impl ToString for CompressionType {
fn to_string(&self) -> String {
impl Display for CompressionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Gzip => "gzip".to_string(),
Self::Brotli => "br".to_string(),
Self::Passthrough => String::new(),
Self::Gzip => write!(f, "gzip"),
Self::Brotli => write!(f, "br"),
Self::Passthrough => Ok(()),
}
}
}
@ -192,6 +196,12 @@ impl Route<'_> {
}
}
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
impl Server {
pub fn new() -> Self {
Self {
@ -720,7 +730,7 @@ mod tests {
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
_ => panic!("no decompressor for {}", expected_encoding.to_string()),
_ => panic!("no decompressor for {}", expected_encoding),
};
let mut decompressed = Vec::<u8>::new();

View File

@ -1,12 +1,14 @@
#![allow(clippy::cmp_owned)]
use std::collections::HashMap;
// CRATES
use crate::server::ResponseExt;
use crate::utils::{redirect, template, Preferences};
use askama::Template;
use cookie::Cookie;
use futures_lite::StreamExt;
use hyper::{Body, Request, Response};
use rinja::Template;
use time::{Duration, OffsetDateTime};
// STRUCTS
@ -19,22 +21,27 @@ struct SettingsTemplate {
// CONSTANTS
const PREFS: [&str; 15] = [
const PREFS: [&str; 20] = [
"theme",
"mascot",
"front_page",
"layout",
"wide",
"comment_sort",
"post_sort",
"blur_spoiler",
"show_nsfw",
"blur_nsfw",
"use_hls",
"ffmpeg_video_downloads",
"hide_hls_notification",
"autoplay_videos",
"hide_sidebar_and_summary",
"fixed_navbar",
"hide_awards",
"hide_score",
"disable_visit_reddit_confirmation",
"video_quality",
];
// FUNCTIONS
@ -80,7 +87,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone()))
.path("/")
.http_only(true)
.http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
),
@ -119,7 +126,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone()))
.path("/")
.http_only(true)
.http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
),

View File

@ -1,12 +1,17 @@
#![allow(clippy::cmp_owned)]
use crate::{config, utils};
// CRATES
use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
};
use crate::{client::json, server::ResponseExt, RequestExt};
use askama::Template;
use crate::{client::json, server::RequestExt, server::ResponseExt};
use cookie::Cookie;
use hyper::{Body, Request, Response};
use rinja::Template;
use once_cell::sync::Lazy;
use regex::Regex;
use time::{Duration, OffsetDateTime};
// STRUCTS
@ -50,10 +55,13 @@ struct WallTemplate {
url: String,
}
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
// SERVICES
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path
let root = req.uri().path() == "/";
let query = req.uri().query().unwrap_or_default().to_string();
let subscribed = setting(&req, "subscriptions");
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());
@ -107,10 +115,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let mut params = String::from("&raw_json=1");
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/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
let filters = get_filters(&req);
@ -136,6 +148,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
if sort == "new" {
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
}
Ok(template(&SubredditTemplate {
sub,
posts,
@ -446,8 +462,71 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
})
}
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
description: Some(format!(
"<a href='{}{}'>Comments</a>",
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
post.permalink
)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await;
assert!(subreddit.is_ok());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_gated_and_quarantined() {
let quarantined = subreddit("edgy", true).await;
assert!(quarantined.is_ok());
let gated = subreddit("drugs", true).await;
assert!(gated.is_ok());
}

View File

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

View File

@ -1,19 +1,25 @@
use crate::config::get_setting;
#![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use crate::config::{self, get_setting};
//
// CRATES
//
use crate::{client::json, server::RequestExt};
use askama::Template;
use cookie::Cookie;
use hyper::{Body, Request, Response};
use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
use rinja::Template;
use rust_embed::RustEmbed;
use serde::Serialize;
use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet};
use std::env;
use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url;
@ -43,6 +49,7 @@ pub enum ResourceType {
}
// Post flair with content, background color and foreground color
#[derive(Serialize)]
pub struct Flair {
pub flair_parts: Vec<FlairPart>,
pub text: String,
@ -51,7 +58,7 @@ pub struct Flair {
}
// Part of flair, either emoji or text
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub struct FlairPart {
pub flair_part_type: String,
pub value: String,
@ -93,12 +100,14 @@ impl FlairPart {
}
}
#[derive(Serialize)]
pub struct Author {
pub name: String,
pub flair: Flair,
pub distinguished: String,
}
#[derive(Serialize)]
pub struct Poll {
pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String),
@ -126,6 +135,7 @@ impl Poll {
}
}
#[derive(Serialize)]
pub struct PollOption {
pub id: u64,
pub text: String,
@ -155,18 +165,21 @@ impl PollOption {
}
// Post flags with nsfw and stickied
#[derive(Serialize)]
pub struct Flags {
pub spoiler: bool,
pub nsfw: bool,
pub stickied: bool,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub struct Media {
pub url: String,
pub alt_url: String,
pub width: i64,
pub height: i64,
pub poster: String,
pub download_name: String,
}
impl Media {
@ -233,6 +246,15 @@ impl Media {
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
format!("redlib_{permalink_base}_{media_url_base}")
} else {
String::new()
};
(
post_type.to_string(),
Self {
@ -243,12 +265,14 @@ impl Media {
width: source["width"].as_i64().unwrap_or_default(),
height: source["height"].as_i64().unwrap_or_default(),
poster: format_url(source["url"].as_str().unwrap_or_default()),
download_name,
},
gallery,
)
}
}
#[derive(Serialize)]
pub struct GalleryMedia {
pub url: String,
pub width: i64,
@ -289,6 +313,7 @@ impl GalleryMedia {
}
// Post containing content, metadata and media
#[derive(Serialize)]
pub struct Post {
pub id: String,
pub title: String,
@ -296,6 +321,7 @@ pub struct Post {
pub body: String,
pub author: Author,
pub permalink: String,
pub link_title: String,
pub poll: Option<Poll>,
pub score: (String, String),
pub upvote_ratio: i64,
@ -307,11 +333,13 @@ pub struct Post {
pub domain: String,
pub rel_time: String,
pub created: String,
pub created_ts: u64,
pub num_duplicates: u64,
pub comments: (String, String),
pub gallery: Vec<GalleryMedia>,
pub awards: Awards,
pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String,
}
@ -338,6 +366,7 @@ impl Post {
let data = &post["data"];
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
let score = data["score"].as_i64().unwrap_or_default();
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = val(post, "title");
@ -384,6 +413,7 @@ impl Post {
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(),
download_name: String::new(),
},
media,
domain: val(post, "domain"),
@ -402,22 +432,25 @@ impl Post {
},
},
flags: Flags {
spoiler: data["spoiler"].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(),
},
permalink: val(post, "permalink"),
link_title: val(post, "link_title"),
poll: Poll::parse(&data["poll_data"]),
rel_time,
created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
gallery,
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
});
}
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
}
}
@ -447,7 +480,7 @@ pub struct Comment {
pub prefs: Preferences,
}
#[derive(Default, Clone)]
#[derive(Default, Clone, Serialize)]
pub struct Award {
pub name: String,
pub icon_url: String,
@ -461,6 +494,7 @@ impl std::fmt::Display for Award {
}
}
#[derive(Serialize)]
pub struct Awards(pub Vec<Award>);
impl std::ops::Deref for Awards {
@ -570,14 +604,20 @@ pub struct Params {
#[derive(Default)]
pub struct Preferences {
pub available_themes: Vec<String>,
pub available_mascots: Vec<String>,
pub theme: String,
pub mascot: String,
pub front_page: String,
pub layout: String,
pub wide: String,
pub blur_spoiler: String,
pub show_nsfw: String,
pub blur_nsfw: String,
pub hide_hls_notification: String,
pub video_quality: String,
pub hide_sidebar_and_summary: String,
pub use_hls: String,
pub ffmpeg_video_downloads: String,
pub autoplay_videos: String,
pub fixed_navbar: String,
pub disable_visit_reddit_confirmation: String,
@ -594,6 +634,11 @@ pub struct Preferences {
#[include = "*.css"]
pub struct ThemeAssets;
#[derive(RustEmbed)]
#[folder = "static/mascots/"]
#[include = "*.png"]
pub struct MascotAssets;
impl Preferences {
// Build preferences from cookies
pub fn new(req: &Request<Body>) -> Self {
@ -604,16 +649,29 @@ impl Preferences {
let chunks: Vec<&str> = file.as_ref().split(".css").collect();
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 {
available_themes: themes,
available_mascots: mascots,
theme: setting(req, "theme"),
mascot: setting(req, "mascot"),
front_page: setting(req, "front_page"),
layout: setting(req, "layout"),
wide: setting(req, "wide"),
blur_spoiler: setting(req, "blur_spoiler"),
show_nsfw: setting(req, "show_nsfw"),
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
blur_nsfw: setting(req, "blur_nsfw"),
use_hls: setting(req, "use_hls"),
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
hide_hls_notification: setting(req, "hide_hls_notification"),
video_quality: setting(req, "video_quality"),
autoplay_videos: setting(req, "autoplay_videos"),
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
@ -666,6 +724,8 @@ pub async fn parse_post(post: &Value) -> Post {
// Determine the type of media along with the media URL
let (post_type, media, gallery) = Media::parse(&post["data"]).await;
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
let permalink = val(post, "permalink");
@ -702,6 +762,7 @@ pub async fn parse_post(post: &Value) -> Post {
distinguished: val(post, "distinguished"),
},
permalink,
link_title: val(post, "link_title"),
poll,
score: format_num(score),
upvote_ratio: ratio as i64,
@ -713,6 +774,7 @@ pub async fn parse_post(post: &Value) -> Post {
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
poster: String::new(),
download_name: String::new(),
},
flair: Flair {
flair_parts: FlairPart::parse(
@ -729,18 +791,21 @@ pub async fn parse_post(post: &Value) -> Post {
},
},
flags: Flags {
spoiler: post["data"]["spoiler"].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),
},
domain: val(post, "domain"),
rel_time,
created,
created_ts,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
gallery,
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
}
}
@ -875,32 +940,162 @@ pub fn format_url(url: &str) -> String {
// 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_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 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
pub fn rewrite_urls(input_text: &str) -> String {
let text1 =
let mut text1 =
// Rewrite Reddit links to Redlib
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
.to_string();
let text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
// Remove (html-encoded) "\" from URLs.
.replace("%5C", "")
.replace("\\_", "_");
loop {
if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
break;
} else {
text1 = REDDIT_EMOJI_REGEX
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
}
}
// Remove (html-encoded) "\" from URLs.
text1 = text1.replace("%5C", "").replace("\\_", "_");
// Rewrite external media previews to Redlib
if REDDIT_PREVIEW_REGEX.is_match(&text1) {
REDDIT_PREVIEW_REGEX
.replace_all(&text1, format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
} else {
text1
loop {
if REDDIT_PREVIEW_REGEX.find(&text1).is_none() {
return text1;
} else {
let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default());
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()
}
}
}
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
/* Create the paths we'll use to look for our data inside the json.
Because we don't know the name of any given emote we use a wildcard to parse them. */
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
// Extract all of the results from those json paths
let link_nodes = media_metadata.json_path(&link_path);
let id_nodes = media_metadata.json_path(&id_path);
// Initialize our vectors
let mut id_vec = Vec::new();
let mut link_vec = Vec::new();
// Add the relevant data to each of our vectors so we can access it by number later
for current_id in id_nodes {
id_vec.push(current_id)
}
for current_link in link_nodes {
link_vec.push(current_link)
}
/* Set index to the length of link_vec.
This is one larger than we'll actually be looking at, but we correct that later */
let mut index = link_vec.len();
// Comment needs to be in scope for when we call rewrite_urls()
let mut comment = comment;
/* Loop until index hits zero.
This also prevents us from trying to do anything on an empty vector */
while index != 0 {
/* Subtract 1 from index to get the real index we should be looking at.
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
index -= 1;
// Convert our current index in id_vec into a string so we can search through it with regex
let current_id = id_vec[index].to_string();
/* The ID number can be multiple lengths, so we capture it with regex.
We also want to only attempt anything when we get matches to avoid panicking */
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(&current_id) {
// Format the ID to include the colons it has in the comment text
let id = format!(":{}:", &id_capture[1]);
// Convert current link to string to search through it with the regex
let link = link_vec[index].to_string();
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
It's a good idea and makes everything look nicer, so we'll do the same. */
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
let to_replace_with = format!(
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
&link_capture[1]
);
// Inside the comment replace the ID we found with the string that will embed the image
comment = comment.replace(&id, &to_replace_with).to_string();
}
}
}
// Call rewrite_urls() to transform any other Reddit links
rewrite_urls(&comment)
}
// Format vote count to a string that will be displayed.
// Append `m` and `k` for millions and thousands respectively, and
// round to the nearest tenth.
@ -980,7 +1175,7 @@ pub fn redirect(path: &str) -> Response<Body> {
/// Renders a generic error landing page.
pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
error!("Error page rendered: {msg}");
error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
let url = req.uri().to_string();
let body = ErrorTemplate {
msg: msg.to_string(),
@ -1007,6 +1202,28 @@ pub fn sfw_only() -> bool {
}
}
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
/// value `on`.
///
/// If this variable is set as such, the instance will block all robots in robots.txt and
/// insert the noindex, nofollow meta tag on every page.
pub fn disable_indexing() -> bool {
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
Some(val) => val == "on",
None => false,
}
}
// Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only();
@ -1048,6 +1265,34 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
}
// Returns the last (non-empty) segment of a path string
pub fn url_path_basename(path: &str) -> String {
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
if url_result.is_err() {
path.to_string()
} else {
let mut url = url_result.unwrap();
url.path_segments_mut().unwrap().pop_if_empty();
url.path_segments().unwrap().last().unwrap().to_string()
}
}
// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}
#[cfg(test)]
mod tests {
use super::{format_num, format_url, rewrite_urls};
@ -1148,3 +1393,35 @@ async fn test_fetching_ws() {
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);
}
#[test]
fn test_url_path_basename() {
// without trailing slash
assert_eq!(url_path_basename("/first/last"), "last");
// with trailing slash
assert_eq!(url_path_basename("/first/last/"), "last");
// with query parameters
assert_eq!(url_path_basename("/first/last/?some=query"), "last");
// file path
assert_eq!(url_path_basename("/cdn/image.jpg"), "image.jpg");
// when a full url is passed instead of just a path
assert_eq!(url_path_basename("https://doma.in/first/last"), "last");
// empty path
assert_eq!(url_path_basename("/"), "");
}
#[test]
fn test_rewriting_emotes() {
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 60 KiB

35
static/check_update.js Normal file
View File

@ -0,0 +1,35 @@
async function checkInstanceUpdateStatus() {
try {
const response = await fetch('/commits.json');
const text = await response.text();
const entries = JSON.parse(text);
const localCommit = document.getElementById('git_commit').dataset.value;
let statusMessage = '';
if (entries.length > 0) {
const commitHashes = Array.from(entries).map(entry => {
return entry.sha
});
const commitIndex = commitHashes.indexOf(localCommit);
if (commitIndex === 0) {
statusMessage = '✅ Instance is up to date.';
} else if (commitIndex > 0) {
statusMessage = `⚠️ This instance is not up to date and is ${commitIndex} commits old. Test and confirm on an up-to-date instance before reporting.`;
} else {
statusMessage = `⚠️ This instance is not up to date and is at least ${commitHashes.length} commits old. Test and confirm on an up-to-date instance before reporting.`;
}
} else {
statusMessage = '⚠️ Unable to fetch commit information.';
}
document.getElementById('update-status').innerText = statusMessage;
} catch (error) {
console.error('Error fetching commits:', error);
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
}
}
checkInstanceUpdateStatus();

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

1
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,110 +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) === "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);
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);
});
}
function addQualitySelector(videoElement, hlsInstance, availableLevels) {
var qualitySelector = document.createElement('select');
qualitySelector.classList.add('quality-selector');
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

File diff suppressed because it is too large Load Diff

View File

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

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

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

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);
}

View File

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

View File

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

View File

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

View File

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

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, {type: 'video/mp4'});
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,13 +1,16 @@
{% import "utils.html" as utils %}
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if prefs.fixed_navbar == "on" %}fixed_navbar{% endif %}">
<head>
{% block head %}
<title>{% block title %}Redlib{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="description" content="View on Redlib, an alternative private front-end to Reddit.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if crate::utils::disable_indexing() %}
<meta name="robots" content="noindex, nofollow">
{% endif %}
<!-- General PWA -->
<meta name="theme-color" content="#1F1F1F">
<!-- iOS Application -->
@ -24,18 +27,20 @@
<link rel="manifest" type="application/json" href="/manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}">
<!-- Video quality -->
<div id="video_quality" data-value="{{ prefs.video_quality }}"></div>
{% endblock %}
</head>
<body class="
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
{% if prefs.wide == "on" %} wide{% endif %}
{% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %} wide{% endif %}
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<!-- NAVIGATION BAR -->
<nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<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 %}
</div>
{% block search %}{% endblock %}
@ -60,6 +65,13 @@
</div>
</nav>
{% if prefs.mascot != "none" && prefs.mascot != "" %}
<!-- MASCOT -->
<div class="mascot">
<img src="/mascot/{{ prefs.mascot }}.png">
</div>
{% endif %}
<!-- MAIN CONTENT -->
{% block body %}
<main>
@ -76,7 +88,7 @@
<a href="/info" title="View instance information">ⓘ View instance info</a>
</div>
<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>
</footer>
{% endblock %}

View File

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

View File

@ -2,10 +2,19 @@
{% block title %}Error: {{ msg }}{% endblock %}
{% block sortstyle %}{% endblock %}
{% block content %}
<div id="error">
<h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br />
<h3>Head back <a href="/">home</a>?</h3>
</div>
<div id="error">
<h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br />
<h3 id="update-status"></h3>
<br>
<div id="git_commit" data-value="{{ crate::instance_info::INSTANCE_INFO.git_commit }}"></div>
<script src="/check_update.js"></script>
<h3>Expected something to work? Try a <a href="https://github.com/redlib-org/redlib/">upstream</a> instance.</h3>
<br />
<h3 id="issue_warning" >!! Do <b>NOT</b> open an issue on the redlib repository with a redsunlib specific issue !!</h3>
<br />
<h3>Head back <a href="/">home</a>?</h3>
</div>
{% endblock %}

View File

@ -6,11 +6,11 @@
<h1>
&#128561;
{% 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 %}
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 %}
This post is NSFW!
This post is <b class="nsfw-tag">NSFW</b>!
{% endif %}
</h1>
<br />
@ -20,6 +20,7 @@
This instance of Redlib is SFW-only.</p>
{% 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>
<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 %}
{% endif %}
</p>

View File

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

View File

@ -3,6 +3,10 @@
{% block title %}Redlib Settings{% endblock %}
{% block subscriptions %}
{% call utils::sub_list("") %}
{% endblock %}
{% block search %}
{% call utils::search("".to_owned(), "") %}
{% endblock %}
@ -19,6 +23,12 @@
{% call utils::options(prefs.theme, prefs.available_themes, "system") %}
</select>
</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>
<legend>Interface</legend>
@ -31,17 +41,39 @@
<div class="prefs-group">
<label for="layout">Layout:</label>
<select name="layout" id="layout">
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
{% call utils::options(prefs.layout, ["card", "clean", "compact", "old", "waterfall"], "card") %}
</select>
</div>
<div class="prefs-group">
<label for="wide">Wide UI:</label>
<input type="hidden" value="off" name="wide">
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
</div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
<input type="hidden" value="off" name="hide_sidebar_and_summary">
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset>
<fieldset>
<legend>Content</legend>
<div class="prefs-group">
<label for="video_quality">Video quality:</label>
<select name="video_quality" id="video_quality">
{% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %}
</select>
</div>
<div class="prefs-group">
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
<select name="post_sort">
@ -54,6 +86,11 @@
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select>
</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() %}
<div class="prefs-group">
<label for="show_nsfw">Show NSFW posts:</label>
@ -66,30 +103,6 @@
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
</div>
{% endif %}
<div class="prefs-group">
<label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="fixed_navbar">Keep navbar fixed</label>
<input type="hidden" value="off" name="fixed_navbar">
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="use_hls">Use HLS for videos</label>
<details id="feeds">
<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>
</details>
<input type="hidden" value="off" name="use_hls">
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
<input type="hidden" value="off" name="hide_hls_notification">
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="hide_awards">Hide awards</label>
<input type="hidden" value="off" name="hide_awards">
@ -100,15 +113,48 @@
<input type="hidden" value="off" name="hide_score">
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
</div>
</fieldset>
<fieldset>
<legend>Media</legend>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
<label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="use_hls">Use HLS for videos</label>
{% if prefs.ffmpeg_video_downloads != "on" %}
<details id="feeds">
<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>
</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="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 class="prefs-group">
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
<input type="hidden" value="off" name="hide_hls_notification">
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
</div>
</fieldset>
<input id="save" type="submit" value="Save">
</div>
</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 %}
<div class="prefs" id="settings_subs">
<legend>Subscribed Feeds</legend>
@ -139,11 +185,6 @@
{% endfor %}
</div>
{% 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>
{% endblock %}

View File

@ -64,25 +64,29 @@
{% call utils::post_in_list(post) %}
{% endif %}
{% 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="/playHLSVideo.js"></script>
<script src="/videoUtils.js"></script>
{% endif %}
</div>
{% endif %}
<footer>
{% 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 %}
{% 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 %}
</footer>
</div>
{% 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>
{% if is_filtered %}
<center>(Content from r/{{ sub.name }} has been filtered)</center>
@ -130,10 +134,16 @@
</form>
{% endif %}
</div>
</div>
{% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
</div>
{% endif %}
</div>
</details>
<details class="panel" id="sidebar">
<details class="panel" id="sidebar" open>
<summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents">
{{ sub.info|safe }}

View File

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

View File

@ -62,8 +62,9 @@
{%- endmacro %}
{% macro post(post) -%}
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
<!-- POST CONTENT -->
<div class="post highlighted">
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
<p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span>
@ -86,13 +87,14 @@
{% endif %}
</p>
<h1 class="post_title">
{{ post.title }}
{% if post.flair.flair_parts.len() > 0 %}
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
class="post_flair"
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %}
{{ post.title }}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
</h1>
<!-- POST MEDIA -->
@ -101,7 +103,7 @@
<div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image" >
{% 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 }}"/>
{% else %}
<svg
@ -117,7 +119,11 @@
</a>
</div>
{% 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>
<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>
@ -125,7 +131,7 @@
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
</div>
<script src="/playHLSVideo.js"></script>
<script src="/videoUtils.js"></script>
{% else %}
<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>
@ -151,7 +157,10 @@
{% endif %}
<!-- POST BODY -->
<div class="post_body">{{ post.body|safe }}</div>
<div class="post_body">
{{ post.body|safe }}
{% call poll(post) %}
</div>
<div class="post_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %}
{{ post.score.0 }}
@ -161,13 +170,32 @@
<span class="label"> Upvotes</span></div>
<div class="post_footer">
<ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li>
<li>
<a href="{{ post.permalink }}">
<span class="desktop_item">perma</span>link
</a>
</li>
{% if post.num_duplicates > 0 %}
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
<li>
<a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
dup<span class="desktop_item">licat</span>es
</a>
</li>
{% endif %}
{% if post.post_type == "link" %}
<li class="desktop_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive.is</a></li>
<li class="mobile_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive</a></li>
{% endif %}
{% call external_reddit_link(post.permalink) %}
{% if post.media.download_name != "" %}
<li>
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
<span class="mobile_item">dl</span>
<span class="desktop_item">download</span>
</a>
</li>
{% endif %}
</ul>
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
</div>
@ -175,8 +203,7 @@
{%- endmacro %}
{% macro external_reddit_link(permalink) %}
{% for dev_type in ["desktop", "mobile"] %}
<li class="{{ dev_type }}_item">
<li>
<a
{% if prefs.disable_visit_reddit_confirmation != "on" %}
href="#popup"
@ -190,11 +217,11 @@
{% call visit_reddit_confirmation(permalink) %}
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% macro post_in_list(post) -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
<p class="post_header">
{% let community -%}
{% if post.community.starts_with("u_") -%}
@ -222,10 +249,10 @@
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
{% 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>
<!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
{% if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && post.post_type == "image" %}
<div class="post_media_content">
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
{% if post.media.height == 0 || post.media.width == 0 %}
@ -233,7 +260,6 @@
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
{% else %}
<svg
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
width="{{ post.media.width }}px"
height="{{ post.media.height }}px"
xmlns="http://www.w3.org/2000/svg">
@ -245,26 +271,22 @@
{% endif %}
</a>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || post.post_type == "video") %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
</div>
{% else %}
<div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
<video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %}
{% else if post.post_type != "self" %}
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
<a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
{% if post.thumbnail.url.is_empty() %}
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
<title>Thumbnail</title>
@ -272,7 +294,7 @@
</svg>
{% else %}
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc>
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>