Compare commits

...

102 Commits

Author SHA1 Message Date
aa54301054 Upgrade to version 0.23 2022-10-31 20:35:00 -07:00
b4d3f03335 Upgrade dependencies 2022-10-31 20:23:59 -07:00
1a1ff2e600 Use singular form of "comment" for posts with 1 comment (#567)
* Use singular form of "comment" for posts with 1 comment

* Fix incorrect text on comment count tooltip

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-10-31 18:36:24 -07:00
4fc07c02b5 update instance (igna.rocks => intent.cool) (#603)
* update instance (igna.rocks => intent.cool)

* Remove accidentally-added broken instances

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-10-31 18:22:20 -07:00
8d58cf61d2 Removed 8 dead links from instance list (#545)
* Update README.md

Removed the following instances for dead links:

* libreddit.sugoma.tk
* libreddit.jamiethalacker.dev
* libreddit.database.red
* reddit.phii.me
* libreddit.autarkic.org
* lr.oversold.host
* libreddit.datatunnel.xyz
* libreddit.crewz.me

* Fix double pipe on flux.industries instance

Co-authored-by: Mohammed Anas <triallax@tutanota.com>

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-10-31 18:06:32 -07:00
711e3c205d Add libreddit.cachyos.org instance (#571)
Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-10-31 17:50:59 -07:00
0704eb10b8 Add new instance (libreddit.oxymagnesium.com) (#591) 2022-10-31 17:49:20 -07:00
ef86c1be86 Remove reddit.artemislena.eu (uses Teddit, not Libreddit now) (#586)
* L: Fixed two swapped config variables in the documentation.

* L: reddit.artemislena.eu is now Teddit, not Libreddit anymore
2022-10-31 17:46:16 -07:00
8141b74817 Update Esmail's onion instance (#593) 2022-10-31 17:45:05 -07:00
57d304161b Add ~vern onion instance (#537)
* Add ~vern onion instance

 Add vern.cc's onion instance of libreddit to instance list

* Remove http:// from link name

* Fix previous commit
2022-06-24 10:52:08 -07:00
b5f21bcb97 Added 3 instances (#531)
* Add Instance - encrypted-data.xyz

* Add Instance - eu.org

* Add Instance - opnxng.com
2022-06-24 10:50:16 -07:00
36c560144a Remove libreddit.awesomehub.io (#535) 2022-06-24 10:48:41 -07:00
2bc714d0c5 Added mha.fi Instance (#519)
Close #518
2022-06-11 20:41:54 +00:00
ff4a515e24 change lr.vern.cc hosting location as we moved vps (#526) 2022-06-11 20:39:29 +00:00
93f089c2cf Add libreddit.foss.wtf (Close #527) 2022-06-11 20:39:11 +00:00
23569206cc L: Fixed two swapped config variables in the documentation. (#524) 2022-06-06 02:09:36 +00:00
5f20e8ee27 Fix dark theme hidden in settings 2022-05-28 19:55:13 -07:00
a8a8980b98 Update README.md (#516) 2022-05-28 05:31:38 +00:00
fd7d977835 Add instance rd.jae.su (close #515) 2022-05-28 05:31:07 +00:00
50f26333cb remove 40two.app - dead/serves ads (#517)
40two.app looks like it serves ads instead of libreddit. Hasn't worked for 1 week+
2022-05-28 05:17:38 +00:00
f5cd48b07f Fix #514 2022-05-21 21:06:03 -07:00
50665bbeb3 Switch titles to <h1>s (Fixes #444) 2022-05-21 15:47:58 -07:00
d558127306 Add keyboard shortcuts to nav buttons (closes #466) 2022-05-20 23:10:11 -07:00
0c757023f9 Correct localhost to 0.0.0.0 in SystemD conf (#498) 2022-05-21 05:53:48 +00:00
90828cc71c Fix "Post url contains non-ASCII characters" error (#479) 2022-05-21 05:48:59 +00:00
7f5bfc04b3 Always show Feeds dropdown (Fixes #408) 2022-05-20 22:42:05 -07:00
322aa97a18 Fix HTML encoding in templating (#404) 2022-05-21 05:28:31 +00:00
7e07ca3df1 Fix #480 2022-05-20 21:26:53 -07:00
428dc58e3c Update to v0.22.8 2022-05-20 19:20:44 -07:00
0ec8e4e9a2 Harden Systemd configuration (#453) 2022-05-21 01:48:32 +00:00
60c7b6b23f Embed css themes to simplify adding and testing new themes (#489) 2022-05-21 01:41:31 +00:00
1c8bcf33c1 Made .onion instance url consistant (#511)
Without `http://`, it may have caused problems for libredirect
2022-05-18 04:30:51 +00:00
3bdc21f90a Remove silkky.cloud instance. Closes #510 2022-05-17 03:12:03 +00:00
c3dade257d Restore post sorting preference by link (#406) 2022-05-17 03:11:01 +00:00
62b2bbb231 Add reddit.dr460nf1r3.org instance (#504)
Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-05-15 21:37:39 +00:00
653aee9294 CI: Add docker build caching (#493) 2022-05-15 21:37:13 +00:00
bb7fb1313d Fix multireddit subscription redirect url 2022-05-15 13:50:17 -07:00
01bc729a80 ✏️ Fix link to Cloudflare in README.md (#506)
Added `.com` to `https://cloudflare` to fix the link in `README.md`.
2022-05-15 19:58:32 +00:00
39e6e6bf81 Add lr.vern.cc to instance list (#505) 2022-05-15 19:58:10 +00:00
8c94c0dd17 Let native elements use theme accent colour (#509) 2022-05-15 19:56:25 +00:00
1c50c8f30d Update instances
Closes #503, #495, #488, #484, #483, #454, and #507
2022-05-15 19:16:15 +00:00
3facaefb53 Add strongthany.cc instance (#478)
* Update to list of public instances

added strongthany.cc instance to the list of public instances

* fixed accidental removal

put the location status back for riverside.
2022-04-06 18:25:11 +00:00
aec45311cc ✍️fix: Tries to add better readability - ➡️ fix: Moves funding links to correct file (#477)
* ✍️ fix(readme): Adds better readability

* ➡️ fix(buymeacoffee): Moves custom link to correct FUNDING.yml

* ➡️ fix(buymeacoffee): Moves custom link to correct FUNDING.yml

* ✍️ fix(bugreport): Adds better readability

* ✍️ fix(featureparity): Adds better readability

* ✍️ fix(featurerequest): Adds better readability

* ✍️ fix(crypto): Removes broken linking, changed to codeblock
2022-04-06 18:24:10 +00:00
47ab857103 Scroll overflowing tables (fixes #469) 2022-04-02 21:24:20 -07:00
a9ef5bc08b Add lunar.icu instance. Closes #460 2022-03-27 00:50:19 +00:00
eb6c5e5e1e Fix backslash url rewriting and add tests for rewrite_urls. (#461)
* Fix backslash url rewriting.

Add test for rewrite_urls.

Fixes #281.

* Update to v0.22.5

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2022-03-26 20:26:30 +00:00
ed11135af8 Update to v0.22.4 2022-03-26 13:09:24 -07:00
3a1af78e26 Wrap long post urls. (#462) 2022-03-26 19:55:53 +00:00
345770c64d Update libreddit.privacy.com.de domain 2022-03-26 19:54:26 +00:00
9eb42932df Hide empty sidebar 2022-03-24 21:19:21 -07:00
f0a6bdc21b Fix sorting buttons on r/all and r/popular (#402)
* Fix sorting buttons on r/all and r/popular

* Bump version to v0.22.2

* Fix empty sidebar in r/all and r/popular

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2022-03-15 03:39:39 +00:00
3eef60d486 Add instances (#432, #433, #436, #438) 2022-03-14 01:01:09 +00:00
59043456ba Wrap long post titles (fixes #435) 2022-03-13 12:59:15 -07:00
90c7088da2 Link Privacy Redirect as well 2022-03-13 19:19:56 +00:00
9e65a65556 Promote LibRedirect in README 2022-03-13 19:15:47 +00:00
8cfbde2710 Add LiberaPay "Donate" button back 2022-03-13 19:11:39 +00:00
70ff150ab4 Add user listing buttons (#400)
* Add user listing buttons

* Update to v0.22

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2022-03-13 19:06:27 +00:00
388779c1f2 Update instances (#421)
close #411 
close #412 
close #417

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-03-13 18:38:35 +00:00
6b605d859f Add German leddit.xyz instance (#429)
* new DE instance

* new hidden service

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-03-13 18:37:01 +00:00
0ae48c400c Add kylrth instances (#446)
Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-03-13 18:35:13 +00:00
a6ed18d674 Changed location of my VPS :) (#415)
* Changed location of my VPS :)

So my tor url changed too along side my VPS's country so yup!

* Update README.md
2022-03-13 18:32:26 +00:00
838cdd95d1 Update libreddit.drivet.xyz (#360)
* Remove Cloudflare Proxy from libreddit.drivet.xyz

I'm testing some stuff and as a result i have disabled proxy for libreddit.drivet.xyz. It exposes my public ip, but also gives more privacy i guess

* Move libreddit.drivet.xyz to Poland

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2022-03-13 18:32:10 +00:00
bc95b08ffd Update libreddit.datatunnel.xyz to Finland 2022-01-21 17:36:34 +00:00
e6190267e4 Add libreddit.datatunnel.xyz instance. Closes #401 2022-01-20 22:34:47 +00:00
3ceeac5fb0 Add lr.rfl890.cf instance. Closes #399 2022-01-19 18:14:51 +00:00
60eb0137c2 Add libreddit.bus-hit.me to instances (#398) 2022-01-19 18:13:46 +00:00
b6bca68d4e Add reddi.tk instance. Closes #397 2022-01-17 20:13:38 +00:00
91bff826f0 Fix and improve admin/mod distinguishers (#386)
* Fix regression with comments from deleted mods

Starting with https://github.com/spikecodes/libreddit/pull/367/files
comments from deleted moderators and admins(?) aren't highlighted.

* Highlight mod and admin usernames in posts

Works like on reddit + shows highlight for mods on the search page.
2022-01-09 02:50:53 +00:00
af6606a855 leddit.xyz instance location change (#387) 2022-01-09 01:14:05 +00:00
977cd0763a Fix #379 2022-01-05 16:46:45 -08:00
fcadd44cb3 Update dependencies 2022-01-05 16:39:56 -08:00
9c325c2cbf Search fixes (#384)
* Default to searching within subreddit

* Redirect to subreddit from search
2022-01-05 14:06:41 -08:00
e9038f4fe2 Add stilic.ml instance. Closes #380 2021-12-31 20:33:53 +00:00
8b8f55e09a Fix sort button scrollbars 2021-12-31 10:42:44 -08:00
f1b3749cf0 Fix #378 — formatting of dates/times 2021-12-29 12:48:57 -08:00
0708fdfb37 Cover more Reddit domains with libreddit link rewrites 2021-12-29 11:38:35 -08:00
cad29e9544 Add libreddit.nl instance. Closes #377 2021-12-28 16:16:53 +00:00
6b59976fcf Fix #376 2021-12-27 23:16:01 -08:00
f9b3981448 Fix debug log in post.rs 2021-12-27 19:56:37 -08:00
db3196df5a Use Reveddit to show removed posts/comments. Closes #299 2021-12-27 19:40:35 -08:00
b3d4f6f91c Show external page links again 2021-12-27 18:00:19 -08:00
45b875b85d Continue Rust workflow if version is already published 2021-12-27 14:21:06 -08:00
992d7889c4 Automatically publish to crates.io 2021-12-27 13:53:58 -08:00
3188f9d8e7 Tweak settings page design 2021-12-27 13:43:44 -08:00
90fa0b5496 Automatically generate release notes 2021-12-27 10:15:25 -08:00
7aeabfc4bc Rewrite Reddit post links to Libreddit equivalents 2021-12-26 21:18:20 -08:00
150ebe38f3 Add Buy Me a Coffee button as donation option 2021-12-27 01:38:14 +00:00
2905d114fa Add Buy Me a Coffee donation option 2021-12-27 00:48:56 +00:00
40e97cc75d Add esmailelbob.xyz instance (#369)
* add new instance

I want to add my own instance - i'm new to selfhosting so i can't guarantee it will be running all the time but i can guarantee whenever it's down i will fix it within hours!

* Fix onion protocol for instance

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2021-12-25 05:11:30 +00:00
7c73e352ce Fix [deleted] user link color 2021-12-19 17:12:33 -08:00
341c623be8 Refactor Media parsing (#334)
* Parse video data from cross_post_parent_list as vanilla Reddit does.

introduce testdata directory for testing JSON parsing functions.

refactor Media::parse for slightly more readability.

Add various test cases.

* Trim down to just refactoring

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2021-12-20 01:07:20 +00:00
4c8b724a9d Merge pull request #367 from alyaeanyx/no-href-to-deleted-user
Don't create hrefs to u/[deleted]
2021-12-19 16:58:04 -08:00
227d74b187 Add totaldarkness.net instance. Closes #366 2021-12-20 00:50:20 +00:00
f05a818edd Don't create hrefs to u/[deleted] 2021-12-19 12:20:37 +01:00
ceee13cfb7 docs: add the missing AUTOPLAY_VIDEOS in README
Merge pull request #361 from xatier/patch-2
2021-12-14 22:03:43 -08:00
a39495b3cb Update README.md
This config was introduced in 1d4ea50a45 , but missed from the documentation.
2021-12-12 14:41:00 -08:00
38cfe4ad71 Add libreddit.hu instsance. Closes #357 2021-12-08 23:13:09 +00:00
0b89539c2b Add lr.cowfee.moe instance. Closes #353 2021-12-01 03:44:44 +00:00
046b8b3edc Add new onion instance. Closes #349 2021-12-01 03:42:54 +00:00
0656756d21 Fix #196 2021-11-29 22:29:41 -08:00
43551f70fd Add leddit onion instance 2021-11-30 04:09:41 +00:00
364c29c4d5 Use resized icons for awards. Fixes #346 2021-11-28 14:47:50 -08:00
41 changed files with 1114 additions and 886 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@ -1,7 +1,7 @@
--- ---
name: 🐛 Bug report name: 🐛 Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: '🐛 Bug Report: '
labels: bug labels: bug
assignees: '' assignees: ''
@ -12,7 +12,7 @@ assignees: ''
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
--> -->
## To reproduce ## Steps to reproduce the bug
<!-- <!--
Steps to reproduce the behavior: Steps to reproduce the behavior:
@ -22,12 +22,12 @@ Steps to reproduce the behavior:
4. See error 4. See error
--> -->
## Expected behavior ## What's the expected behavior?
<!-- <!--
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
--> -->
## Additional context ## Additional context / screenshot
<!-- <!--
Add any other context about the problem here. Add any other context about the problem here.
--> -->

View File

@ -1,7 +1,7 @@
--- ---
name: ✨ Feature parity name: ✨ Feature parity
about: Suggest implementing a feature into Libreddit that is found in Reddit.com about: Suggest implementing a feature into Libreddit that is found in Reddit.com
title: '' title: '✨ Feature parity: '
labels: feature parity labels: feature parity
assignees: '' assignees: ''
@ -12,7 +12,7 @@ assignees: ''
A clear and concise description of what the feature is. A clear and concise description of what the feature is.
--> -->
## Describe the implementation into Libreddit ## Describe how this could be implemented into Libreddit
<!-- <!--
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
--> -->
@ -22,7 +22,7 @@ assignees: ''
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or features you've considered.
--> -->
## Additional context ## Additional context / screenshot
<!-- <!--
Add any other context or screenshots about the feature parity request here. Add any other context or screenshots about the feature parity request here.
--> -->

View File

@ -1,7 +1,7 @@
--- ---
name: 💡 Feature request name: 💡 Feature request
about: Suggest a feature for Libreddit that is not found in Reddit about: Suggest a feature for Libreddit that is not found in Reddit
title: '' title: '💡 Feature request: '
labels: enhancement labels: enhancement
assignees: '' assignees: ''
@ -12,7 +12,7 @@ assignees: ''
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
--> -->
## Describe the solution you'd like ## Describe the feature you would like to be implemented
<!-- <!--
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
--> -->
@ -22,7 +22,7 @@ assignees: ''
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or features you've considered.
--> -->
## Additional context ## Additional context / screenshot
<!-- <!--
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
--> -->

View File

@ -34,3 +34,5 @@ jobs:
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: spikecodes/libreddit:arm tags: spikecodes/libreddit:arm
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -37,3 +37,5 @@ jobs:
platforms: linux/arm/v7 platforms: linux/arm/v7
push: true push: true
tags: spikecodes/libreddit:armv7 tags: spikecodes/libreddit:armv7
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -34,4 +34,5 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: spikecodes/libreddit:latest tags: spikecodes/libreddit:latest
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -22,6 +22,10 @@ jobs:
- name: Build - name: Build
run: cargo build --release run: cargo build --release
- name: Publish to crates.io
continue-on-error: true
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- uses: actions/upload-artifact@v2.2.1 - uses: actions/upload-artifact@v2.2.1
name: Upload a Build Artifact name: Upload a Build Artifact
@ -43,12 +47,13 @@ jobs:
if: github.base_ref != 'master' if: github.base_ref != 'master'
with: with:
tag_name: ${{ steps.version.outputs.version }} tag_name: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }} - NAME name: ${{ steps.version.outputs.version }} - ${{ github.event.head_commit.message }}
draft: true draft: true
files: | files: |
target/release/libreddit target/release/libreddit
libreddit.sha512 libreddit.sha512
body: | body: |
- ${{ github.event.head_commit.message }} ${{ github.sha }} - ${{ github.event.head_commit.message }} ${{ github.sha }}
generate_release_notes: true
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

1013
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,23 +3,25 @@ name = "libreddit"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0" license = "AGPL-3.0"
repository = "https://github.com/spikecodes/libreddit" repository = "https://github.com/spikecodes/libreddit"
version = "0.20.0" version = "0.23.0"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018" edition = "2021"
[dependencies] [dependencies]
askama = { version = "0.10.5", default-features = false } askama = { version = "0.11.1", default-features = false }
async-recursion = "0.3.2" async-recursion = "1.0.0"
cached = "0.26.2" cached = "0.40.0"
clap = { version = "2.33.3", default-features = false } clap = { version = "4.0.18", default-features = false, features = ["std"] }
regex = "1.5.4" regex = "1.6.0"
serde = { version = "1.0.130", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }
cookie = "0.15.1" cookie = "0.16.1"
futures-lite = "1.12.0" futures-lite = "1.12.0"
hyper = { version = "0.14.15", features = ["full"] } hyper = { version = "0.14.22", features = ["full"] }
hyper-rustls = "0.23.0" hyper-rustls = "0.23.0"
percent-encoding = "2.2.0"
route-recognizer = "0.3.1" route-recognizer = "0.3.1"
serde_json = "1.0.72" serde_json = "1.0.87"
tokio = { version = "1.14.0", features = ["full"] } tokio = { version = "1.21.2", features = ["full"] }
time = "0.2.7" time = "0.3.16"
url = "2.2.2" url = "2.3.1"
rust-embed = { version = "6.4.2", features = ["include-exclude"] }

View File

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: spike
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

108
README.md
View File

@ -8,7 +8,7 @@
**10 second pitch:** Libreddit is a portmanteau of "libre" (meaning freedom) and "Reddit". It is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit). **10 second pitch:** Libreddit is a portmanteau of "libre" (meaning freedom) and "Reddit". It is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit).
- 🚀 Fast: written in Rust for blazing fast speeds and memory safety - 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat - ☁️ Light: no JavaScript, no ads, no tracking, no bloat
- 🕵 Private: all requests are proxied through the server, including media - 🕵 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 - 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
@ -17,11 +17,13 @@
I appreciate any donations! Your support allows me to continue developing Libreddit. I appreciate any donations! Your support allows me to continue developing Libreddit.
**Liberapay:** <a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a> <a href="https://www.buymeacoffee.com/spikecodes" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px" ></a>
<a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" style="height: 40px"></a>
**Bitcoin:** [bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y](bitcoin:bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y)
**Monero:** [45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR](monero:45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR) **Bitcoin:** `bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y`
**Monero:** `45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR`
--- ---
@ -29,39 +31,62 @@ I appreciate any donations! Your support allows me to continue developing Libred
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here! Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here!
🔗 **Want to automatically redirect Reddit links to Libreddit? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
| Website | Country | Cloudflare | | Website | Country | Cloudflare |
|-|-|-| |-|-|-|
| [libredd.it](https://libredd.it) (official) | 🇺🇸 US | | | [libredd.it](https://libredd.it) (official) | 🇺🇸 US | |
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | | | [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇩🇪 DE | ✅ | | [libreddit.dothq.co](https://libreddit.dothq.co) | 🇩🇪 DE | ✅ |
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | | | [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | |
| [libreddit.40two.app](https://libreddit.40two.app) | 🇳🇱 NL | |
| [reddit.invak.id](https://reddit.invak.id) | 🇧🇬 BG | | | [reddit.invak.id](https://reddit.invak.id) | 🇧🇬 BG | |
| [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
| [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | | | [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | | | [libreddit.strongthany.cc](https://libreddit.strongthany.cc) | 🇺🇸 US | |
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | | | [libreddit.privacy.com.de](https://libreddit.privacy.com.de) | 🇩🇪 DE | |
| [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
| [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ | | [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ |
| [libreddit.sugoma.tk](https://libreddit.sugoma.tk) | 🇺🇸 US | |
| [libreddit.jamiethalacker.dev](https://libreddit.jamiethalacker.dev) | 🇺🇸 US | ✅ |
| [reddit.artemislena.eu](https://reddit.artemislena.eu) | 🇩🇪 DE | |
| [r.nf](https://r.nf) | 🇩🇪 DE | ✅ | | [r.nf](https://r.nf) | 🇩🇪 DE | ✅ |
| [libreddit.awesomehub.io](https://libreddit.awesomehub.io) | 🇫🇮 FI | |
| [libreddit.some-things.org](https://libreddit.some-things.org) | 🇨🇭 CH | | | [libreddit.some-things.org](https://libreddit.some-things.org) | 🇨🇭 CH | |
| [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | | | [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | |
| [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | | | [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | |
| [libreddit.alefvanoon.xyz](https://libreddit.alefvanoon.xyz) | 🇺🇸 US | | | [libreddit.intent.cool](https://libreddit.intent.cool) | 🇺🇸 US | |
| [libreddit.igna.rocks](https://libreddit.igna.rocks) | 🇺🇸 US | | | [libreddit.drivet.xyz](https://libreddit.drivet.xyz) | 🇵🇱 PL | |
| [libreddit.autarkic.org](https://libreddit.autarkic.org) | 🇺🇸 US | |
| [libreddit.flux.industries](https://libreddit.flux.industries) | 🇩🇪 DE | ✅ |
| [libreddit.drivet.xyz](https://libreddit.drivet.xyz) | 🇫🇮 FI | ✅ |
| [lr.oversold.host](https://lr.oversold.host) | 🇱🇺 LU | |
| [libreddit.de](https://libreddit.de) | 🇩🇪 DE | | | [libreddit.de](https://libreddit.de) | 🇩🇪 DE | |
| [libreddit.pussthecat.org](https://libreddit.pussthecat.org) | 🇩🇪 DE | | | [libreddit.pussthecat.org](https://libreddit.pussthecat.org) | 🇩🇪 DE | |
| [libreddit.mutahar.rocks](https://libreddit.mutahar.rocks) | 🇫🇷 FR | | | [libreddit.mutahar.rocks](https://libreddit.mutahar.rocks) | 🇫🇷 FR | |
| [libreddit.northboot.xyz](https://libreddit.northboot.xyz) | 🇩🇪 DE | | | [libreddit.northboot.xyz](https://libreddit.northboot.xyz) | 🇩🇪 DE | |
| [leddit.xyz](https://www.leddit.xyz) | 🇩🇪 DE | | | [leddit.xyz](https://leddit.xyz) | 🇺🇸 US | |
| [de.leddit.xyz](https://de.leddit.xyz) | 🇩🇪 DE | |
| [lr.cowfee.moe](https://lr.cowfee.moe) | 🇺🇸 US | |
| [libreddit.hu](https://libreddit.hu) | 🇫🇮 FI | ✅ |
| [libreddit.totaldarkness.net](https://libreddit.totaldarkness.net) | 🇨🇦 CA | |
| [libreddit.esmailelbob.xyz](https://libreddit.esmailelbob.xyz) | 🇨🇦 CA | |
| [lr.vern.cc](https://lr.vern.cc) | 🇨🇦 CA | |
| [libreddit.nl](https://libreddit.nl) | 🇳🇱 NL | |
| [lr.stilic.ml](https://lr.stilic.ml) | 🇫🇷 FR | ✅ |
| [reddi.tk](https://reddi.tk) | 🇺🇸 US | ✅ |
| [libreddit.bus-hit.me](https://libreddit.bus-hit.me) | 🇨🇦 CA | |
| [r.walkx.org](https://r.walkx.org) | 🇳🇱 NL | ✅ |
| [libreddit.kylrth.com](https://libreddit.kylrth.com) | 🇨🇦 CA | |
| [libreddit.yonalee.eu](https://libreddit.yonalee.eu) | 🇱🇺 LU | ✅ |
| [libreddit.winscloud.net](https://libreddit.winscloud.net) | 🇹🇭 TH | ✅ |
| [libreddit.tiekoetter.com](https://libreddit.tiekoetter.com) | 🇩🇪 DE | |
| [reddit.rtrace.io](https://reddit.rtrace.io) | 🇩🇪 DE | |
| [libreddit.lunar.icu](https://libreddit.lunar.icu) | 🇩🇪 DE | ✅ |
| [libreddit.privacydev.net](https://libreddit.privacydev.net) | 🇺🇸 US | |
| [libreddit.notyourcomputer.net](https://libreddit.notyourcomputer.net) | 🇺🇸 US | |
| [r.ahwx.org](https://r.ahwx.org) | 🇳🇱 NL | ✅ |
| [bob.fr.to](https://bob.fr.to) | 🇺🇸 US | |
| [reddit.beparanoid.de](https://reddit.beparanoid.de) | 🇨🇭 CH | |
| [libreddit.dcs0.hu](https://libreddit.dcs0.hu) | 🇭🇺 HU | |
| [reddit.dr460nf1r3.org](https://reddit.dr460nf1r3.org) | 🇩🇪 DE | ✅ |
| [rd.jae.su](https://rd.jae.su) | 🇫🇮 FI | |
| [libreddit.mha.fi](https://libreddit.mha.fi) | 🇫🇮 FI | |
| [libreddit.foss.wtf](https://libreddit.foss.wtf) | 🇩🇪 DE | |
| [libreddit.encrypted-data.xyz](https://libreddit.encrypted-data.xyz)| 🇫🇷 FR | ✅ |
| [libreddit.eu.org](https://libreddit.eu.org)| 🇮🇪 IE | ✅ |
| [l.opnxng.com](https://l.opnxng.com)| 🇸🇬 SG | |
| [libreddit.cachyos.org](https://libreddit.cachyos.org) | 🇩🇪 DE | ✅ |
| [libreddit.oxymagnesium.com](https://libreddit.oxymagnesium.com) | 🇺🇸 US | |
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | | | [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | | | [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | | | [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
@ -69,9 +94,17 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
| [liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion](http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion) | 🇩🇪 DE | | | [liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion](http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion) | 🇩🇪 DE | |
| [kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion](http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion) | 🇺🇸 US | | | [kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion](http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion) | 🇺🇸 US | |
| [ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion](http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion) | 🇩🇪 DE | | | [ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion](http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion) | 🇩🇪 DE | |
| [ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion](http://ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion) | 🇺🇸 US | |
| [libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion](http://libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion) | 🇺🇸 US | |
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website. | [libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion](http://libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion) | 🇪🇬 EG | |
| [ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion](http://ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion) | 🇩🇪 DE | |
| [lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion](http://lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion) | 🇨🇦 CA | |
| [libreddit.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://libreddit.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | 🇨🇦 CA | |
| [reddit.prnoid54e44a4bduq5due64jkk7wcnkxcp5kv3juncm7veptjcqudgyd.onion](http://reddit.prnoid54e44a4bduq5due64jkk7wcnkxcp5kv3juncm7veptjcqudgyd.onion) | 🇨🇭 CH | |
| [inz6tbezfwzexva6dize4cqraj2tjdhygxabmcgysccesvw2pybzhbyd.onion](http://inz6tbezfwzexva6dize4cqraj2tjdhygxabmcgysccesvw2pybzhbyd.onion) | 🇫🇮 FI | |
| [libreddit.micohauwkjbyw5meacrb4ipicwvwg4xtzl7y7viv53kig2mdcsvwkyyd.onion](http://libreddit.micohauwkjbyw5meacrb4ipicwvwg4xtzl7y7viv53kig2mdcsvwkyyd.onion/)| 🇫🇮 FI | |
| [lr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://lr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇨🇦 CA | |
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site that uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
--- ---
@ -134,7 +167,7 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot
- The requested URL - The requested URL
- Search terms - Search terms
**Location:** The same privacy policy goes on to describe location data may be collected through the use of: **Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
- GPS (consensual) - GPS (consensual)
- Bluetooth (consensual) - Bluetooth (consensual)
- Content associated with a location (consensual) - Content associated with a location (consensual)
@ -158,7 +191,7 @@ For transparency, I hope to describe all the ways Libreddit handles user privacy
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data. **Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data.
**Hosting:** The official instances are hosted on [Replit](https://replit.com/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting, using unofficial instances and browsing through Tor are welcomed. **Hosting:** The official instances are 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.
--- ---
@ -224,17 +257,18 @@ libreddit
Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value | | Name | Possible values | Default value |
|-------------------------|------------------------------------------------------------------------------------------|---------------| |-------------------------|-----------------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"]` | `system` | | `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` | | `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` | | `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` | | `WIDE` | `["on", "off"]` | `off` |
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` | | `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` | | `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` | | `SHOW_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` | | `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` | | `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
### Examples ### Examples
@ -248,7 +282,7 @@ LIBREDDIT_DEFAULT_WIDE=on LIBREDDIT_DEFAULT_THEME=dark libreddit -r
## Proxying using NGINX ## Proxying using NGINX
**NOTE** If you're [proxying Libreddit through a NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add **NOTE** If you're [proxying Libreddit through an NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
```nginx ```nginx
proxy_http_version 1.1; proxy_http_version 1.1;
``` ```

View File

@ -1,2 +1,2 @@
ADDRESS=localhost ADDRESS=0.0.0.0
PORT=12345 PORT=12345

View File

@ -11,5 +11,27 @@ Environment=PORT=8080
EnvironmentFile=-/etc/libreddit.conf EnvironmentFile=-/etc/libreddit.conf
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT} ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
# Hardening
DeviceAllow=
LockPersonality=yes
MemoryDenyWriteExecute=yes
PrivateDevices=yes
ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service ~@privileged ~@resources
UMask=0077
[Install] [Install]
WantedBy=default.target WantedBy=default.target

View File

@ -1,8 +1,9 @@
use cached::proc_macro::cached; use cached::proc_macro::cached;
use futures_lite::{future::Boxed, FutureExt}; use futures_lite::{future::Boxed, FutureExt};
use hyper::{body::Buf, client, Body, Request, Response, Uri}; use hyper::{body::Buf, client, Body, Request, Response, Uri};
use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value; use serde_json::Value;
use std::{result::Result, str::FromStr}; use std::result::Result;
use crate::server::RequestExt; use crate::server::RequestExt;
@ -20,7 +21,7 @@ pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, S
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> { async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
// First parameter is target URL (mandatory). // First parameter is target URL (mandatory).
let url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?; let uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Prepare the HTTPS connector. // Prepare the HTTPS connector.
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build(); let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
@ -28,7 +29,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
// Build the hyper client from the HTTPS connector. // Build the hyper client from the HTTPS connector.
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https); let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
let mut builder = Request::get(url); let mut builder = Request::get(uri);
// Copy useful headers from original request // Copy useful headers from original request
for &key in &["Range", "If-Modified-Since", "Cache-Control"] { for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
@ -89,7 +90,10 @@ fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String
response response
.headers() .headers()
.get("Location") .get("Location")
.map(|val| val.to_str().unwrap_or_default()) .map(|val| {
let new_url = percent_encode(val.as_bytes(), CONTROLS).to_string();
format!("{}{}raw_json=1", new_url, if new_url.contains('?') { "&" } else { "?" })
})
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
quarantine, quarantine,

View File

@ -1,13 +1,6 @@
// Global specifiers // Global specifiers
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(clippy::pedantic, clippy::all)] #![allow(clippy::cmp_owned)]
#![allow(
clippy::needless_pass_by_value,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::manual_find_map,
clippy::unused_async
)]
// Reference local files // Reference local files
mod post; mod post;
@ -18,7 +11,7 @@ mod user;
mod utils; mod utils;
// Import Crates // Import Crates
use clap::{App as cli, Arg}; use clap::{Arg, Command};
use futures_lite::FutureExt; use futures_lite::FutureExt;
use hyper::{header::HeaderValue, Body, Request, Response}; use hyper::{header::HeaderValue, Body, Request, Response};
@ -26,7 +19,7 @@ use hyper::{header::HeaderValue, Body, Request, Response};
mod client; mod client;
use client::proxy; use client::proxy;
use server::RequestExt; use server::RequestExt;
use utils::{error, redirect}; use utils::{error, redirect, ThemeAssets};
mod server; mod server;
@ -70,6 +63,7 @@ async fn font() -> Result<Response<Body>, String> {
Response::builder() Response::builder()
.status(200) .status(200)
.header("content-type", "font/woff2") .header("content-type", "font/woff2")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(include_bytes!("../static/Inter.var.woff2").as_ref().into()) .body(include_bytes!("../static/Inter.var.woff2").as_ref().into())
.unwrap_or_default(), .unwrap_or_default(),
) )
@ -91,50 +85,67 @@ async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Respons
Ok(res) Ok(res)
} }
async fn style() -> Result<Response<Body>, String> {
let mut res = include_str!("../static/style.css").to_string();
for file in ThemeAssets::iter() {
res.push('\n');
let theme = ThemeAssets::get(file.as_ref()).unwrap();
res.push_str(std::str::from_utf8(theme.data.as_ref()).unwrap());
}
Ok(
Response::builder()
.status(200)
.header("content-type", "text/css")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(res.to_string().into())
.unwrap_or_default(),
)
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let matches = cli::new("Libreddit") let matches = Command::new("Libreddit")
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about("Private front-end for Reddit written in Rust ") .about("Private front-end for Reddit written in Rust ")
.arg( .arg(
Arg::with_name("redirect-https") Arg::new("redirect-https")
.short("r") .short('r')
.long("redirect-https") .long("redirect-https")
.help("Redirect all HTTP requests to HTTPS (no longer functional)") .help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false), .num_args(0),
) )
.arg( .arg(
Arg::with_name("address") Arg::new("address")
.short("a") .short('a')
.long("address") .long("address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Sets address to listen on") .help("Sets address to listen on")
.default_value("0.0.0.0") .default_value("0.0.0.0")
.takes_value(true), .num_args(1),
) )
.arg( .arg(
Arg::with_name("port") Arg::new("port")
.short("p") .short('p')
.long("port") .long("port")
.value_name("PORT") .value_name("PORT")
.help("Port to listen on") .help("Port to listen on")
.default_value("8080") .default_value("8080")
.takes_value(true), .num_args(1),
) )
.arg( .arg(
Arg::with_name("hsts") Arg::new("hsts")
.short("H") .short('H')
.long("hsts") .long("hsts")
.value_name("EXPIRE_TIME") .value_name("EXPIRE_TIME")
.help("HSTS header to tell browsers that this site should only be accessed over HTTPS") .help("HSTS header to tell browsers that this site should only be accessed over HTTPS")
.default_value("604800") .default_value("604800")
.takes_value(true), .num_args(1),
) )
.get_matches(); .get_matches();
let address = matches.value_of("address").unwrap_or("0.0.0.0"); let address = matches.get_one("address").map(|m: &String| m.as_str()).unwrap_or("0.0.0.0");
let port = std::env::var("PORT").unwrap_or_else(|_| matches.value_of("port").unwrap_or("8080").to_string()); let port = std::env::var("PORT").unwrap_or_else(|_| matches.get_one("port").map(|m: &String| m.as_str()).unwrap_or("8080").to_string());
let hsts = matches.value_of("hsts"); let hsts = matches.get_one("hsts").map(|m: &String| m.as_str());
let listener = [address, ":", &port].concat(); let listener = [address, ":", &port].concat();
@ -158,7 +169,7 @@ async fn main() {
} }
// Read static files // Read static files
app.at("/style.css").get(|_| resource(include_str!("../static/style.css"), "text/css", false).boxed()); app.at("/style.css").get(|_| style().boxed());
app app
.at("/manifest.json") .at("/manifest.json")
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed()); .get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
@ -183,6 +194,9 @@ async fn main() {
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed()); app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed()); app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed()); app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
app
.at("/preview/:loc/award_images/:fullname/:id")
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed()); app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed());
app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed()); app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed());
app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed()); app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed());
@ -196,6 +210,7 @@ async fn main() {
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed()); app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed()); app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed()); app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
app.at("/user/:name/comments/:id/:title").get(|r| post::item(r).boxed()); app.at("/user/:name/comments/:id/:title").get(|r| post::item(r).boxed());
app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed()); app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());

View File

@ -1,6 +1,5 @@
// CRATES // CRATES
use crate::client::json; use crate::client::json;
use crate::esc;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine}; use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{ use crate::utils::{
@ -13,7 +12,7 @@ use std::collections::HashSet;
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
#[template(path = "post.html", escape = "none")] #[template(path = "post.html")]
struct PostTemplate { struct PostTemplate {
comments: Vec<Comment>, comments: Vec<Comment>,
post: Post, post: Post,
@ -97,12 +96,23 @@ async fn parse_post(json: &serde_json::Value) -> Post {
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]); let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
let permalink = val(post, "permalink");
let body = if val(post, "removed_by_category") == "moderator" {
format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}\">view removed post</a></p></div>",
permalink
)
} else {
rewrite_urls(&val(post, "selftext_html"))
};
// Build a post using data parsed from Reddit post API // Build a post using data parsed from Reddit post API
Post { Post {
id: val(post, "id"), id: val(post, "id"),
title: esc!(post, "title"), title: val(post, "title"),
community: val(post, "subreddit"), community: val(post, "subreddit"),
body: rewrite_urls(&val(post, "selftext_html")).replace("\\", ""), body,
author: Author { author: Author {
name: val(post, "author"), name: val(post, "author"),
flair: Flair { flair: Flair {
@ -111,13 +121,13 @@ async fn parse_post(json: &serde_json::Value) -> Post {
post["data"]["author_flair_richtext"].as_array(), post["data"]["author_flair_richtext"].as_array(),
post["data"]["author_flair_text"].as_str(), post["data"]["author_flair_text"].as_str(),
), ),
text: esc!(post, "link_flair_text"), text: val(post, "link_flair_text"),
background_color: val(post, "author_flair_background_color"), background_color: val(post, "author_flair_background_color"),
foreground_color: val(post, "author_flair_text_color"), foreground_color: val(post, "author_flair_text_color"),
}, },
distinguished: val(post, "distinguished"), distinguished: val(post, "distinguished"),
}, },
permalink: val(post, "permalink"), permalink,
score: format_num(score), score: format_num(score),
upvote_ratio: ratio as i64, upvote_ratio: ratio as i64,
post_type, post_type,
@ -135,7 +145,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
post["data"]["link_flair_richtext"].as_array(), post["data"]["link_flair_richtext"].as_array(),
post["data"]["link_flair_text"].as_str(), post["data"]["link_flair_text"].as_str(),
), ),
text: esc!(post, "link_flair_text"), text: val(post, "link_flair_text"),
background_color: val(post, "link_flair_background_color"), background_color: val(post, "link_flair_background_color"),
foreground_color: if val(post, "link_flair_text_color") == "dark" { foreground_color: if val(post, "link_flair_text_color") == "dark" {
"black".to_string() "black".to_string()
@ -174,7 +184,6 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time); let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
let score = data["score"].as_i64().unwrap_or(0); let score = data["score"].as_i64().unwrap_or(0);
let body = rewrite_urls(&val(&comment, "body_html"));
// If this comment contains replies, handle those too // If this comment contains replies, handle those too
let replies: Vec<Comment> = if data["replies"].is_object() { let replies: Vec<Comment> = if data["replies"].is_object() {
@ -191,6 +200,15 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
let id = val(&comment, "id"); let id = val(&comment, "id");
let highlighted = id == highlighted_comment; let highlighted = id == highlighted_comment;
let body = if val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]" {
format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}{}\">view removed comment</a></p></div>",
post_link, id
)
} else {
rewrite_urls(&val(&comment, "body_html"))
};
let author = Author { let author = Author {
name: val(&comment, "author"), name: val(&comment, "author"),
flair: Flair { flair: Flair {
@ -199,7 +217,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
data["author_flair_richtext"].as_array(), data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(), data["author_flair_text"].as_str(),
), ),
text: esc!(&comment, "link_flair_text"), text: val(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"), background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"), foreground_color: val(&comment, "author_flair_text_color"),
}, },

View File

@ -29,7 +29,7 @@ struct Subreddit {
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "search.html", escape = "none")] #[template(path = "search.html")]
struct SearchTemplate { struct SearchTemplate {
posts: Vec<Post>, posts: Vec<Post>,
subreddits: Vec<Subreddit>, subreddits: Vec<Subreddit>,
@ -47,13 +47,17 @@ struct SearchTemplate {
// SERVICES // SERVICES
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" }; let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
let path = format!("{}.json?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
let query = param(&path, "q").unwrap_or_default(); let query = param(&path, "q").unwrap_or_default();
if query.is_empty() { if query.is_empty() {
return Ok(redirect("/".to_string())); return Ok(redirect("/".to_string()));
} }
if query.starts_with("r/") {
return Ok(redirect(format!("/{}", query)));
}
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let quarantined = can_access_quarantine(&req, &sub); let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits // Handle random subreddits
@ -78,7 +82,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
// If all requested subs are filtered, we don't need to fetch posts. // If all requested subs are filtered, we don't need to fetch posts.
if sub.split("+").all(|s| filters.contains(s)) { if sub.split('+').all(|s| filters.contains(s)) {
template(SearchTemplate { template(SearchTemplate {
posts: Vec::new(), posts: Vec::new(),
subreddits, subreddits,

View File

@ -105,7 +105,7 @@ impl ResponseExt for Response<Body> {
fn remove_cookie(&mut self, name: String) { fn remove_cookie(&mut self, name: String) {
let mut cookie = Cookie::named(name); let mut cookie = Cookie::named(name);
cookie.set_path("/"); cookie.set_path("/");
cookie.set_max_age(Duration::second()); cookie.set_max_age(Duration::seconds(1));
if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) { if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) {
self.headers_mut().append("Set-Cookie", val); self.headers_mut().append("Set-Cookie", val);
} }
@ -158,8 +158,8 @@ impl Server {
Ok::<_, String>(service_fn(move |req: Request<Body>| { Ok::<_, String>(service_fn(move |req: Request<Body>| {
let headers = default_headers.clone(); let headers = default_headers.clone();
// Remove double slashes // Remove double slashes and decode encoded slashes
let mut path = req.uri().path().replace("//", "/"); let mut path = req.uri().path().replace("//", "/").replace("%2F","/");
// Remove trailing slashes // Remove trailing slashes
if path != "/" && path.ends_with('/') { if path != "/" && path.ends_with('/') {

View File

@ -1,5 +1,4 @@
// CRATES // CRATES
use crate::esc;
use crate::utils::{ use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
}; };
@ -11,7 +10,7 @@ use time::{Duration, OffsetDateTime};
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
#[template(path = "subreddit.html", escape = "none")] #[template(path = "subreddit.html")]
struct SubredditTemplate { struct SubredditTemplate {
sub: Subreddit, sub: Subreddit,
posts: Vec<Post>, posts: Vec<Post>,
@ -19,6 +18,7 @@ struct SubredditTemplate {
ends: (String, String), ends: (String, String),
prefs: Preferences, prefs: Preferences,
url: String, url: String,
redirect_url: String,
/// Whether the subreddit itself is filtered. /// Whether the subreddit itself is filtered.
is_filtered: bool, is_filtered: bool,
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place, /// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
@ -27,7 +27,7 @@ struct SubredditTemplate {
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "wiki.html", escape = "none")] #[template(path = "wiki.html")]
struct WikiTemplate { struct WikiTemplate {
sub: String, sub: String,
wiki: String, wiki: String,
@ -37,7 +37,7 @@ struct WikiTemplate {
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "wall.html", escape = "none")] #[template(path = "wall.html")]
struct WallTemplate { struct WallTemplate {
title: String, title: String,
sub: String, sub: String,
@ -86,22 +86,21 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
} else { } else {
Subreddit::default() Subreddit::default()
} }
} else if sub_name.contains('+') { } else {
// Multireddit // Multireddit, all, popular
Subreddit { Subreddit {
name: sub_name.clone(), name: sub_name.clone(),
..Subreddit::default() ..Subreddit::default()
} }
} else {
Subreddit::default()
}; };
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default()); let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default());
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
let filters = get_filters(&req); let filters = get_filters(&req);
// If all requested subs are filtered, we don't need to fetch posts. // If all requested subs are filtered, we don't need to fetch posts.
if sub_name.split("+").all(|s| filters.contains(s)) { if sub_name.split('+').all(|s| filters.contains(s)) {
template(SubredditTemplate { template(SubredditTemplate {
sub, sub,
posts: Vec::new(), posts: Vec::new(),
@ -109,6 +108,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
ends: (param(&path, "after").unwrap_or_default(), "".to_string()), ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
prefs: Preferences::new(req), prefs: Preferences::new(req),
url, url,
redirect_url,
is_filtered: true, is_filtered: true,
all_posts_filtered: false, all_posts_filtered: false,
}) })
@ -124,6 +124,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
ends: (param(&path, "after").unwrap_or_default(), after), ends: (param(&path, "after").unwrap_or_default(), after),
prefs: Preferences::new(req), prefs: Preferences::new(req),
url, url,
redirect_url,
is_filtered: false, is_filtered: false,
all_posts_filtered, all_posts_filtered,
}) })
@ -211,7 +212,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
.unwrap_or_default(); .unwrap_or_default();
// Find each subreddit name (separated by '+') in sub parameter // Find each subreddit name (separated by '+') in sub parameter
for part in sub.split('+') { for part in sub.split('+').filter(|x| x != &"") {
// Retrieve display name for the subreddit // Retrieve display name for the subreddit
let display; let display;
let part = if part.starts_with("u_") { let part = if part.starts_with("u_") {
@ -253,7 +254,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
// Redirect back to subreddit // Redirect back to subreddit
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar // check for redirect parameter if unsubscribing/unfiltering from outside sidebar
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") { let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
format!("/{}/", redirect_path) format!("/{}", redirect_path)
} else { } else {
format!("/r/{}", sub) format!("/r/{}", sub)
}; };
@ -334,10 +335,10 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
match json(path, quarantined).await { match json(path, quarantined).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => template(WikiTemplate { Ok(response) => template(WikiTemplate {
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")), wiki: rewrite_urls(&val(&response, "description_html")),
// wiki: format!( // wiki: format!(
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>", // "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
// rewrite_urls(&val(&response, "description_html").replace("\\", "")), // rewrite_urls(&val(&response, "description_html"),
// moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""), // moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""),
// ), // ),
sub, sub,
@ -406,10 +407,10 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() }; let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
Ok(Subreddit { Ok(Subreddit {
name: esc!(&res, "display_name"), name: val(&res, "display_name"),
title: esc!(&res, "title"), title: val(&res, "title"),
description: esc!(&res, "public_description"), description: val(&res, "public_description"),
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")), info: rewrite_urls(&val(&res, "description_html")),
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(), // moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
icon: format_url(&icon), icon: format_url(&icon),
members: format_num(members), members: format_num(members),

View File

@ -1,22 +1,24 @@
// CRATES // CRATES
use crate::client::json; use crate::client::json;
use crate::esc;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User}; use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User};
use askama::Template; use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use time::OffsetDateTime; use time::{macros::format_description, OffsetDateTime};
// STRUCTS // STRUCTS
#[derive(Template)] #[derive(Template)]
#[template(path = "user.html", escape = "none")] #[template(path = "user.html")]
struct UserTemplate { struct UserTemplate {
user: User, user: User,
posts: Vec<Post>, posts: Vec<Post>,
sort: (String, String), sort: (String, String),
ends: (String, String), ends: (String, String),
/// "overview", "comments", or "submitted"
listing: String,
prefs: Preferences, prefs: Preferences,
url: String, url: String,
redirect_url: String,
/// Whether the user themself is filtered. /// Whether the user themself is filtered.
is_filtered: bool, is_filtered: bool,
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place, /// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
@ -26,13 +28,17 @@ struct UserTemplate {
// FUNCTIONS // FUNCTIONS
pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
// Build the Reddit JSON API path // Build the Reddit JSON API path
let path = format!( let path = format!(
"/user/{}.json?{}&raw_json=1", "/user/{}/{}.json?{}&raw_json=1",
req.param("name").unwrap_or_else(|| "reddit".to_string()), req.param("name").unwrap_or_else(|| "reddit".to_string()),
req.uri().query().unwrap_or_default() listing,
req.uri().query().unwrap_or_default(),
); );
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str())); let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26");
// Retrieve other variables from Libreddit request // Retrieve other variables from Libreddit request
let sort = param(&path, "sort").unwrap_or_default(); let sort = param(&path, "sort").unwrap_or_default();
@ -46,8 +52,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
posts: Vec::new(), posts: Vec::new(),
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after").unwrap_or_default(), "".to_string()), ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
listing,
prefs: Preferences::new(req), prefs: Preferences::new(req),
url, url,
redirect_url,
is_filtered: true, is_filtered: true,
all_posts_filtered: false, all_posts_filtered: false,
}) })
@ -62,8 +70,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
posts, posts,
sort: (sort, param(&path, "t").unwrap_or_default()), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after").unwrap_or_default(), after), ends: (param(&path, "after").unwrap_or_default(), after),
listing,
prefs: Preferences::new(req), prefs: Preferences::new(req),
url, url,
redirect_url,
is_filtered: false, is_filtered: false,
all_posts_filtered, all_posts_filtered,
}) })
@ -82,7 +92,8 @@ async fn user(name: &str) -> Result<User, String> {
// Send a request to the url // Send a request to the url
json(path, false).await.map(|res| { json(path, false).await.map(|res| {
// Grab creation date as unix timestamp // Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64; let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH);
// Closure used to parse JSON from Reddit APIs // Closure used to parse JSON from Reddit APIs
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string(); let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
@ -90,11 +101,11 @@ async fn user(name: &str) -> Result<User, String> {
// Parse the JSON output into a User struct // Parse the JSON output into a User struct
User { User {
name: res["data"]["name"].as_str().unwrap_or(name).to_owned(), name: res["data"]["name"].as_str().unwrap_or(name).to_owned(),
title: esc!(about("title")), title: about("title"),
icon: format_url(&about("icon_img")), icon: format_url(&about("icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0), karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"), created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(),
banner: esc!(about("banner_img")), banner: about("banner_img"),
description: about("public_description"), description: about("public_description"),
} }
}) })

View File

@ -1,15 +1,16 @@
// //
// CRATES // CRATES
// //
use crate::{client::json, esc, server::RequestExt}; use crate::{client::json, server::RequestExt};
use askama::Template; use askama::Template;
use cookie::Cookie; use cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use regex::Regex; use regex::Regex;
use rust_embed::RustEmbed;
use serde_json::Value; use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use time::{Duration, OffsetDateTime}; use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url; use url::Url;
// Post flair with content, background color and foreground color // Post flair with content, background color and foreground color
@ -21,6 +22,7 @@ pub struct Flair {
} }
// Part of flair, either emoji or text // Part of flair, either emoji or text
#[derive(Clone)]
pub struct FlairPart { pub struct FlairPart {
pub flair_part_type: String, pub flair_part_type: String,
pub value: String, pub value: String,
@ -40,7 +42,7 @@ impl FlairPart {
Self { Self {
flair_part_type: value("e").to_string(), flair_part_type: value("e").to_string(),
value: match value("e") { value: match value("e") {
"text" => esc!(value("t")), "text" => value("t").to_string(),
"emoji" => format_url(value("u")), "emoji" => format_url(value("u")),
_ => String::new(), _ => String::new(),
}, },
@ -53,7 +55,7 @@ impl FlairPart {
"text" => match text_flair { "text" => match text_flair {
Some(text) => vec![Self { Some(text) => vec![Self {
flair_part_type: "text".to_string(), flair_part_type: "text".to_string(),
value: esc!(text), value: text.to_string(),
}], }],
None => Vec::new(), None => Vec::new(),
}, },
@ -74,6 +76,7 @@ pub struct Flags {
pub stickied: bool, pub stickied: bool,
} }
#[derive(Debug)]
pub struct Media { pub struct Media {
pub url: String, pub url: String,
pub alt_url: String, pub alt_url: String,
@ -86,28 +89,29 @@ impl Media {
pub async fn parse(data: &Value) -> (String, Self, Vec<GalleryMedia>) { pub async fn parse(data: &Value) -> (String, Self, Vec<GalleryMedia>) {
let mut gallery = Vec::new(); let mut gallery = Vec::new();
// Define the various known places that Reddit might put video URLs.
let data_preview = &data["preview"]["reddit_video_preview"];
let secure_media = &data["secure_media"]["reddit_video"];
let crosspost_parent_media = &data["crosspost_parent_list"][0]["secure_media"]["reddit_video"];
// If post is a video, return the video // If post is a video, return the video
let (post_type, url_val, alt_url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() { let (post_type, url_val, alt_url_val) = if data_preview["fallback_url"].is_string() {
// Return reddit video
( (
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) { if data_preview["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
"gif" &data_preview["fallback_url"],
} else { Some(&data_preview["hls_url"]),
"video"
},
&data["preview"]["reddit_video_preview"]["fallback_url"],
Some(&data["preview"]["reddit_video_preview"]["hls_url"]),
) )
} else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() { } else if secure_media["fallback_url"].is_string() {
// Return reddit video
( (
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) { if secure_media["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
"gif" &secure_media["fallback_url"],
} else { Some(&secure_media["hls_url"]),
"video" )
}, } else if crosspost_parent_media["fallback_url"].is_string() {
&data["secure_media"]["reddit_video"]["fallback_url"], (
Some(&data["secure_media"]["reddit_video"]["hls_url"]), if crosspost_parent_media["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
&crosspost_parent_media["fallback_url"],
Some(&crosspost_parent_media["hls_url"]),
) )
} else if data["post_hint"].as_str().unwrap_or("") == "image" { } else if data["post_hint"].as_str().unwrap_or("") == "image" {
// Handle images, whether GIFs or pics // Handle images, whether GIFs or pics
@ -140,18 +144,12 @@ impl Media {
let source = &data["preview"]["images"][0]["source"]; let source = &data["preview"]["images"][0]["source"];
let url = if post_type == "self" || post_type == "link" {
url_val.as_str().unwrap_or_default().to_string()
} else {
format_url(url_val.as_str().unwrap_or_default())
};
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default())); let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
( (
post_type.to_string(), post_type.to_string(),
Self { Self {
url, url: format_url(url_val.as_str().unwrap_or_default()),
alt_url, alt_url,
width: source["width"].as_i64().unwrap_or_default(), width: source["width"].as_i64().unwrap_or_default(),
height: source["height"].as_i64().unwrap_or_default(), height: source["height"].as_i64().unwrap_or_default(),
@ -220,24 +218,19 @@ pub struct Post {
impl Post { impl Post {
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value // Fetch posts of a user or subreddit and return a vector of posts and the "after" value
pub async fn fetch(path: &str, quarantine: bool) -> Result<(Vec<Self>, String), String> { pub async fn fetch(path: &str, quarantine: bool) -> Result<(Vec<Self>, String), String> {
let res;
let post_list;
// Send a request to the url // Send a request to the url
match json(path.to_string(), quarantine).await { let res = match json(path.to_string(), quarantine).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => { Ok(response) => response,
res = response;
}
// If the Reddit API returns an error, exit this function // If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg), Err(msg) => return Err(msg),
} };
// Fetch the list of posts from the JSON response // Fetch the list of posts from the JSON response
match res["data"]["children"].as_array() { let post_list = match res["data"]["children"].as_array() {
Some(list) => post_list = list, Some(list) => list,
None => return Err("No posts found".to_string()), None => return Err("No posts found".to_string()),
} };
let mut posts: Vec<Self> = Vec::new(); let mut posts: Vec<Self> = Vec::new();
@ -248,16 +241,16 @@ impl Post {
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default()); let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
let score = data["score"].as_i64().unwrap_or_default(); let score = data["score"].as_i64().unwrap_or_default();
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0; let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = esc!(post, "title"); let title = val(post, "title");
// Determine the type of media along with the media URL // Determine the type of media along with the media URL
let (post_type, media, gallery) = Media::parse(&data).await; let (post_type, media, gallery) = Media::parse(data).await;
let awards = Awards::parse(&data["all_awardings"]); let awards = Awards::parse(&data["all_awardings"]);
// selftext_html is set for text posts when browsing. // selftext_html is set for text posts when browsing.
let mut body = rewrite_urls(&val(post, "selftext_html")); let mut body = rewrite_urls(&val(post, "selftext_html"));
if body == "" { if body.is_empty() {
body = rewrite_urls(&val(post, "body_html")) body = rewrite_urls(&val(post, "body_html"));
} }
posts.push(Self { posts.push(Self {
@ -273,7 +266,7 @@ impl Post {
data["author_flair_richtext"].as_array(), data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(), data["author_flair_text"].as_str(),
), ),
text: esc!(post, "link_flair_text"), text: val(post, "link_flair_text"),
background_color: val(post, "author_flair_background_color"), background_color: val(post, "author_flair_background_color"),
foreground_color: val(post, "author_flair_text_color"), foreground_color: val(post, "author_flair_text_color"),
}, },
@ -301,7 +294,7 @@ impl Post {
data["link_flair_richtext"].as_array(), data["link_flair_richtext"].as_array(),
data["link_flair_text"].as_str(), data["link_flair_text"].as_str(),
), ),
text: esc!(post, "link_flair_text"), text: val(post, "link_flair_text"),
background_color: val(post, "link_flair_background_color"), background_color: val(post, "link_flair_background_color"),
foreground_color: if val(post, "link_flair_text_color") == "dark" { foreground_color: if val(post, "link_flair_text_color") == "dark" {
"black".to_string() "black".to_string()
@ -327,7 +320,7 @@ impl Post {
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "comment.html", escape = "none")] #[template(path = "comment.html")]
// Comment with content, post, score and data/time that it was posted // Comment with content, post, score and data/time that it was posted
pub struct Comment { pub struct Comment {
pub id: String, pub id: String,
@ -384,7 +377,7 @@ impl Awards {
pub fn parse(items: &Value) -> Self { pub fn parse(items: &Value) -> Self {
let parsed = items.as_array().unwrap_or(&Vec::new()).iter().fold(Vec::new(), |mut awards, item| { let parsed = items.as_array().unwrap_or(&Vec::new()).iter().fold(Vec::new(), |mut awards, item| {
let name = item["name"].as_str().unwrap_or_default().to_string(); let name = item["name"].as_str().unwrap_or_default().to_string();
let icon_url = format_url(&item["icon_url"].as_str().unwrap_or_default().to_string()); let icon_url = format_url(item["resized_icons"][0]["url"].as_str().unwrap_or_default());
let description = item["description"].as_str().unwrap_or_default().to_string(); let description = item["description"].as_str().unwrap_or_default().to_string();
let count: i64 = i64::from_str(&item["count"].to_string()).unwrap_or(1); let count: i64 = i64::from_str(&item["count"].to_string()).unwrap_or(1);
@ -403,7 +396,7 @@ impl Awards {
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "error.html", escape = "none")] #[template(path = "error.html")]
pub struct ErrorTemplate { pub struct ErrorTemplate {
pub msg: String, pub msg: String,
pub prefs: Preferences, pub prefs: Preferences,
@ -448,6 +441,7 @@ pub struct Params {
#[derive(Default)] #[derive(Default)]
pub struct Preferences { pub struct Preferences {
pub available_themes: Vec<String>,
pub theme: String, pub theme: String,
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
@ -462,10 +456,23 @@ pub struct Preferences {
pub filters: Vec<String>, pub filters: Vec<String>,
} }
#[derive(RustEmbed)]
#[folder = "static/themes/"]
#[include = "*.css"]
pub struct ThemeAssets;
impl Preferences { impl Preferences {
// Build preferences from cookies // Build preferences from cookies
pub fn new(req: Request<Body>) -> Self { pub fn new(req: Request<Body>) -> Self {
// Read available theme names from embedded css files.
// Always make the default "system" theme available.
let mut themes = vec!["system".to_string()];
for file in ThemeAssets::iter() {
let chunks: Vec<&str> = file.as_ref().split(".css").collect();
themes.push(chunks[0].to_owned())
}
Self { Self {
available_themes: themes,
theme: setting(&req, "theme"), theme: setting(&req, "theme"),
front_page: setting(&req, "front_page"), front_page: setting(&req, "front_page"),
layout: setting(&req, "layout"), layout: setting(&req, "layout"),
@ -484,7 +491,7 @@ impl Preferences {
/// Gets a `HashSet` of filters from the cookie in the given `Request`. /// Gets a `HashSet` of filters from the cookie in the given `Request`.
pub fn get_filters(req: &Request<Body>) -> HashSet<String> { pub fn get_filters(req: &Request<Body>) -> HashSet<String> {
setting(&req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect::<HashSet<String>>() setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect::<HashSet<String>>()
} }
/// Filters a `Vec<Post>` by the given `HashSet` of filters (each filter being a subreddit name or a user name). If a /// Filters a `Vec<Post>` by the given `HashSet` of filters (each filter being a subreddit name or a user name). If a
@ -551,7 +558,7 @@ pub fn format_url(url: &str) -> String {
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" { if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
String::new() String::new()
} else { } else {
Url::parse(url).map_or(String::new(), |parsed| { Url::parse(url).map_or(url.to_string(), |parsed| {
let domain = parsed.domain().unwrap_or_default(); let domain = parsed.domain().unwrap_or_default();
let capture = |regex: &str, format: &str, segments: i16| { let capture = |regex: &str, format: &str, segments: i16| {
@ -586,8 +593,12 @@ pub fn format_url(url: &str) -> String {
} }
match domain { match domain {
"www.reddit.com" => capture(r"https://www\.reddit\.com/(.*)", "/", 1),
"old.reddit.com" => capture(r"https://old\.reddit\.com/(.*)", "/", 1),
"np.reddit.com" => capture(r"https://np\.reddit\.com/(.*)", "/", 1),
"reddit.com" => capture(r"https://reddit\.com/(.*)", "/", 1),
"v.redd.it" => chain!( "v.redd.it" => chain!(
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2), capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$|\?source=fallback))", "/vid/", 2),
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2) capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
), ),
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1), "i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
@ -598,7 +609,7 @@ pub fn format_url(url: &str) -> String {
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1), "external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1), "styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1), "www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
_ => String::new(), _ => url.to_string(),
} }
}) })
} }
@ -606,8 +617,11 @@ pub fn format_url(url: &str) -> String {
// Rewrite Reddit links to Libreddit in body of text // Rewrite Reddit links to Libreddit in body of text
pub fn rewrite_urls(input_text: &str) -> String { pub fn rewrite_urls(input_text: &str) -> String {
let text1 = let text1 = Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#)
Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#).map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string()); .map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string())
// Remove (html-encoded) "\" from URLs.
.replace("%5C", "")
.replace('\\', "");
// Rewrite external media previews to Libreddit // Rewrite external media previews to Libreddit
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| { Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
@ -636,12 +650,12 @@ pub fn format_num(num: i64) -> (String, String) {
// Parse a relative and absolute time from a UNIX timestamp // Parse a relative and absolute time from a UNIX timestamp
pub fn time(created: f64) -> (String, String) { pub fn time(created: f64) -> (String, String) {
let time = OffsetDateTime::from_unix_timestamp(created.round() as i64); let time = OffsetDateTime::from_unix_timestamp(created.round() as i64).unwrap_or(OffsetDateTime::UNIX_EPOCH);
let time_delta = OffsetDateTime::now_utc() - time; let time_delta = OffsetDateTime::now_utc() - time;
// If the time difference is more than a month, show full date // If the time difference is more than a month, show full date
let rel_time = if time_delta > Duration::days(30) { let rel_time = if time_delta > Duration::days(30) {
time.format("%b %d '%y") time.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default()
// Otherwise, show relative date/time // Otherwise, show relative date/time
} else if time_delta.whole_days() > 0 { } else if time_delta.whole_days() > 0 {
format!("{}d ago", time_delta.whole_days()) format!("{}d ago", time_delta.whole_days())
@ -651,7 +665,12 @@ pub fn time(created: f64) -> (String, String) {
format!("{}m ago", time_delta.whole_minutes()) format!("{}m ago", time_delta.whole_minutes())
}; };
(rel_time, time.format("%b %d %Y, %H:%M:%S UTC")) (
rel_time,
time
.format(format_description!("[month repr:short] [day] [year], [hour]:[minute]:[second] UTC"))
.unwrap_or_default(),
)
} }
// val() function used to parse JSON from Reddit APIs // val() function used to parse JSON from Reddit APIs
@ -659,17 +678,6 @@ pub fn val(j: &Value, k: &str) -> String {
j["data"][k].as_str().unwrap_or_default().to_string() j["data"][k].as_str().unwrap_or_default().to_string()
} }
// Escape < and > to accurately render HTML
#[macro_export]
macro_rules! esc {
($f:expr) => {
$f.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
};
($j:expr, $k:expr) => {
$j["data"][$k].as_str().unwrap_or_default().to_string().replace('<', "&lt;").replace('>', "&gt;")
};
}
// //
// NETWORKING // NETWORKING
// //
@ -709,6 +717,7 @@ pub async fn error(req: Request<Body>, msg: String) -> Result<Response<Body>, St
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::format_num; use super::format_num;
use super::rewrite_urls;
#[test] #[test]
fn format_num_works() { fn format_num_works() {
@ -718,4 +727,14 @@ mod tests {
assert_eq!(format_num(1001), ("1.0k".to_string(), "1001".to_string())); assert_eq!(format_num(1001), ("1.0k".to_string(), "1001".to_string()));
assert_eq!(format_num(1_999_999), ("2.0m".to_string(), "1999999".to_string())); assert_eq!(format_num(1_999_999), ("2.0m".to_string(), "1999999".to_string()));
} }
#[test]
fn rewrite_urls_removes_backslashes() {
let comment_body_html =
r#"<a href=\"https://www.reddit.com/r/linux%5C_gaming/comments/x/just%5C_a%5C_test%5C/\">https://www.reddit.com/r/linux\\_gaming/comments/x/just\\_a\\_test/</a>"#;
assert_eq!(
rewrite_urls(comment_body_html),
r#"<a href="https://www.reddit.com/r/linux_gaming/comments/x/just_a_test/">https://www.reddit.com/r/linux_gaming/comments/x/just_a_test/</a>"#
)
}
} }

View File

@ -45,124 +45,7 @@
} }
} }
/* Light theme setting */ /* Other themes are located in the "themes" folder */
.light {
--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);
}
/* Black theme setting */
.black {
--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);
}
/* Dracula theme setting */
.dracula {
--accent: #bd93f9;
--green: #50fa7b;
--text: #f8f8f2;
--foreground: #3d4051;
--background: #282a36;
--outside: #393c4d;
--post: #333544;
--panel-border: 2px solid #44475a;
--highlighted: #4e5267;
--visited: #969692;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Nord theme setting */
.nord {
--accent: #8fbcbb;
--green: #a3be8c;
--text: #eceff4;
--foreground: #3b4252;
--background: #2e3440;
--outside: #434c5e;
--post: #434c5e;
--panel-border: 2px solid #4c566a;
--highlighted: #3b4252;
--visited: #a3a5aa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Laserwave theme setting */
.laserwave {
--accent: #eb64b9;
--green: #74dfc4;
--text: #e0dfe1;
--foreground: #302a36;
--background: #27212e;
--outside: #3e3647;
--post: #3e3647;
--panel-border: 2px solid #2f2738;
--highlighted: #302a36;
--visited: #91889b;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Violet theme setting */
.violet {
--accent: #7c71dd;
--green: #5cff85;
--text: white;
--foreground: #1F2347;
--background: #12152b;
--outside: #181c3a;
--post: #181c3a;
--panel-border: 1px solid #1F2347;
--highlighted: #1F2347;
--visited: #aaa;
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
}
/* Gold theme setting */
.gold {
--accent: #f2aa4c;
--green: #5cff85;
--text: white;
--foreground: #234;
--background: #101820;
--outside: #1b2936;
--post: #1b2936;
--panel-border: 0px solid black;
--highlighted: #234;
--visited: #aaa;
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
}
/* Rosebox theme setting */
.rosebox {
--accent: #a57562;
--green: #a3be8c;
--text: white;
--foreground: #222;
--background: #262626;
--outside: #222;
--post: #222;
--panel-border: 1px solid #222;
--highlighted: #262626;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
/* General */ /* General */
@ -177,6 +60,7 @@
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote, html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
pre, form, fieldset, table, th, td, select, input { pre, form, fieldset, table, th, td, select, input {
accent-color: var(--accent);
margin: 0; margin: 0;
color: var(--text); color: var(--text);
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
@ -465,6 +349,7 @@ aside {
#wiki { #wiki {
background: var(--foreground); background: var(--foreground);
padding: 35px; padding: 35px;
overflow-wrap: anywhere;
} }
#top { #top {
@ -486,8 +371,8 @@ aside {
/* Sorting and Search */ /* Sorting and Search */
select, #search, #sort_options, #inside, #searchbox > *, #sort_submit { select, #search, #sort_options, #listing_options, #inside, #searchbox > *, #sort_submit {
height: 40px; height: 38px;
} }
.search_label { .search_label {
@ -504,7 +389,7 @@ select {
select, #search { select, #search {
border: none; border: none;
padding: 0 15px; padding: 0 10px;
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
@ -562,6 +447,11 @@ button.submit:hover > svg { stroke: var(--accent); }
border-radius: 5px 0px 0px 5px; border-radius: 5px 0px 0px 5px;
} }
#listing_options + #sort_select {
margin-left: 10px;
border-radius: 5px 0px 0px 5px;
}
#search_sort { #search_sort {
background: var(--highlighted); background: var(--highlighted);
border-radius: 5px; border-radius: 5px;
@ -590,15 +480,16 @@ button.submit:hover > svg { stroke: var(--accent); }
margin-bottom: 20px; margin-bottom: 20px;
} }
#sort_options, footer > a { #sort_options, #listing_options, footer > a {
border-radius: 5px; border-radius: 5px;
align-items: center;
box-shadow: var(--shadow); box-shadow: var(--shadow);
background: var(--outside); background: var(--outside);
display: flex; display: flex;
overflow: auto; overflow: hidden;
} }
#sort_options > a, footer > a { #sort_options > a, #listing_options > a, footer > a {
color: var(--text); color: var(--text);
padding: 10px 20px; padding: 10px 20px;
text-align: center; text-align: center;
@ -606,12 +497,12 @@ button.submit:hover > svg { stroke: var(--accent); }
transition: 0.2s background; transition: 0.2s background;
} }
#sort_options > a.selected { #sort_options > a.selected, #listing_options > a.selected {
background: var(--accent); background: var(--accent);
color: var(--foreground); color: var(--foreground);
} }
#sort_options > a:not(.selected):hover { #sort_options > a:not(.selected):hover, #listing_options > a:not(.selected):hover {
background: var(--foreground); background: var(--foreground);
} }
@ -718,7 +609,7 @@ a.search_subreddit:hover {
} }
.post_score { .post_score {
padding-top: 16px; padding-top: 19px;
padding-left: 12px; padding-left: 12px;
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
@ -747,6 +638,7 @@ a.search_subreddit:hover {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
overflow-wrap: anywhere;
margin: 5px 15px 5px 12px; margin: 5px 15px 5px 12px;
grid-area: post_title; grid-area: post_title;
} }
@ -874,8 +766,9 @@ a.search_subreddit:hover {
#post_url { #post_url {
color: var(--accent); color: var(--accent);
margin: 5px 15px; margin: 5px 12px;
grid-area: post_media; grid-area: post_media;
overflow-wrap: anywhere;
} }
.post_body { .post_body {
@ -884,6 +777,7 @@ a.search_subreddit:hover {
padding: 5px 15px 5px 12px; padding: 5px 15px 5px 12px;
grid-area: post_body; grid-area: post_body;
width: calc(100% - 30px); width: calc(100% - 30px);
overflow-wrap: anywhere;
} }
/* Used only for text post preview */ /* Used only for text post preview */
@ -901,7 +795,7 @@ a.search_subreddit:hover {
opacity: 0.5; opacity: 0.5;
font-size: 14px; font-size: 14px;
grid-area: post_footer; grid-area: post_footer;
margin: 5px 20px 15px 15px; margin: 5px 20px 15px 12px;
} }
.post_comments { .post_comments {
@ -1179,12 +1073,10 @@ summary.comment_data {
color: var(--accent); color: var(--accent);
} }
.prefs { .prefs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center;
padding: 20px; padding: 20px;
background: var(--post); background: var(--post);
border-radius: 5px; border-radius: 5px;
@ -1197,11 +1089,19 @@ summary.comment_data {
width: 100%; width: 100%;
height: 35px; height: 35px;
align-items: center; align-items: center;
margin-top: 10px; margin-top: 7px;
} }
.prefs > p { .prefs legend {
font-weight: 500; font-weight: 500;
border-bottom: 1px solid var(--highlighted);
font-size: 18px;
padding-bottom: 10px;
}
.prefs legend:not(:first-child) {
padding-top: 10px;
margin-top: 15px;
} }
.prefs select { .prefs select {
@ -1303,6 +1203,8 @@ input[type="submit"] {
.md table { .md table {
margin: 5px; margin: 5px;
overflow-x: auto; overflow-x: auto;
display: block;
max-width: fit-content;
} }
.md code { .md code {

14
static/themes/black.css Normal file
View File

@ -0,0 +1,14 @@
/* Black theme setting */
.black {
--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);
}

14
static/themes/dark.css Normal file
View File

@ -0,0 +1,14 @@
/* Dark theme setting */
.dark{
--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);
}

14
static/themes/dracula.css Normal file
View File

@ -0,0 +1,14 @@
/* Dracula theme setting */
.dracula {
--accent: #bd93f9;
--green: #50fa7b;
--text: #f8f8f2;
--foreground: #3d4051;
--background: #282a36;
--outside: #393c4d;
--post: #333544;
--panel-border: 2px solid #44475a;
--highlighted: #4e5267;
--visited: #969692;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

14
static/themes/gold.css Normal file
View File

@ -0,0 +1,14 @@
/* Gold theme setting */
.gold {
--accent: #f2aa4c;
--green: #5cff85;
--text: white;
--foreground: #234;
--background: #101820;
--outside: #1b2936;
--post: #1b2936;
--panel-border: 0px solid black;
--highlighted: #234;
--visited: #aaa;
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
}

View File

@ -0,0 +1,14 @@
/* Laserwave theme setting */
.laserwave {
--accent: #eb64b9;
--green: #74dfc4;
--text: #e0dfe1;
--foreground: #302a36;
--background: #27212e;
--outside: #3e3647;
--post: #3e3647;
--panel-border: 2px solid #2f2738;
--highlighted: #302a36;
--visited: #91889b;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

14
static/themes/light.css Normal file
View File

@ -0,0 +1,14 @@
/* Light theme setting */
.light {
--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);
}

14
static/themes/nord.css Normal file
View File

@ -0,0 +1,14 @@
/* Nord theme setting */
.nord {
--accent: #8fbcbb;
--green: #a3be8c;
--text: #eceff4;
--foreground: #3b4252;
--background: #2e3440;
--outside: #434c5e;
--post: #434c5e;
--panel-border: 2px solid #4c566a;
--highlighted: #3b4252;
--visited: #a3a5aa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

13
static/themes/rosebox.css Normal file
View File

@ -0,0 +1,13 @@
/* Rosebox theme setting */
.rosebox {
--accent: #a57562;
--green: #a3be8c;
--text: white;
--foreground: #222;
--background: #262626;
--outside: #222;
--post: #222;
--panel-border: 1px solid #222;
--highlighted: #262626;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

14
static/themes/violet.css Normal file
View File

@ -0,0 +1,14 @@
/* Violet theme setting */
.violet {
--accent: #7c71dd;
--green: #5cff85;
--text: white;
--foreground: #1F2347;
--background: #12152b;
--outside: #181c3a;
--post: #181c3a;
--panel-border: 1px solid #1F2347;
--highlighted: #1F2347;
--visited: #aaa;
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
}

View File

@ -35,7 +35,7 @@
</div> </div>
{% block search %}{% endblock %} {% block search %}{% endblock %}
<div id="links"> <div id="links">
<a id="reddit_link" href="https://www.reddit.com{{ url }}"> <a id="reddit_link" href="https://www.reddit.com{{ url }}" rel="nofollow">
<span>reddit</span> <span>reddit</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 12.0737C23 10.7308 21.9222 9.64226 20.5926 9.64226C19.9435 9.64226 19.3557 9.90274 18.923 10.3244C17.2772 9.12492 15.0099 8.35046 12.4849 8.26135L13.5814 3.05002L17.1643 3.8195C17.2081 4.73947 17.9539 5.47368 18.8757 5.47368C19.8254 5.47368 20.5951 4.69626 20.5951 3.73684C20.5951 2.77769 19.8254 2 18.8758 2C18.2001 2 17.6214 2.39712 17.3404 2.96952L13.3393 2.11066C13.2279 2.08679 13.1116 2.10858 13.016 2.17125C12.9204 2.23393 12.8533 2.33235 12.8295 2.44491L11.6051 8.25987C9.04278 8.33175 6.73904 9.10729 5.07224 10.3201C4.63988 9.90099 4.05398 9.64226 3.40757 9.64226C2.0781 9.64226 1 10.7308 1 12.0737C1 13.0618 1.58457 13.9105 2.4225 14.2909C2.38466 14.5342 2.36545 14.78 2.36505 15.0263C2.36505 18.7673 6.67626 21.8 11.9945 21.8C17.3131 21.8 21.6243 18.7673 21.6243 15.0263C21.6243 14.7794 21.6043 14.5359 21.5678 14.2957C22.4109 13.9175 23 13.0657 23 12.0737Z"/> <path d="M23 12.0737C23 10.7308 21.9222 9.64226 20.5926 9.64226C19.9435 9.64226 19.3557 9.90274 18.923 10.3244C17.2772 9.12492 15.0099 8.35046 12.4849 8.26135L13.5814 3.05002L17.1643 3.8195C17.2081 4.73947 17.9539 5.47368 18.8757 5.47368C19.8254 5.47368 20.5951 4.69626 20.5951 3.73684C20.5951 2.77769 19.8254 2 18.8758 2C18.2001 2 17.6214 2.39712 17.3404 2.96952L13.3393 2.11066C13.2279 2.08679 13.1116 2.10858 13.016 2.17125C12.9204 2.23393 12.8533 2.33235 12.8295 2.44491L11.6051 8.25987C9.04278 8.33175 6.73904 9.10729 5.07224 10.3201C4.63988 9.90099 4.05398 9.64226 3.40757 9.64226C2.0781 9.64226 1 10.7308 1 12.0737C1 13.0618 1.58457 13.9105 2.4225 14.2909C2.38466 14.5342 2.36545 14.78 2.36505 15.0263C2.36505 18.7673 6.67626 21.8 11.9945 21.8C17.3131 21.8 21.6243 18.7673 21.6243 15.0263C21.6243 14.7794 21.6043 14.5359 21.5678 14.2957C22.4109 13.9175 23 13.0657 23 12.0737Z"/>

View File

@ -10,7 +10,11 @@
</div> </div>
<details class="comment_right" {% if !collapsed || highlighted %}open{% endif %}> <details class="comment_right" {% if !collapsed || highlighted %}open{% endif %}>
<summary class="comment_data"> <summary class="comment_data">
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a> {% if author.name != "[deleted]" %}
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a>
{% else %}
<span class="comment_author {{ author.distinguished }}">u/[deleted]</span>
{% endif %}
{% if author.flair.flair_parts.len() > 0 %} {% if author.flair.flair_parts.len() > 0 %}
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small> <small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
{% endif %} {% endif %}
@ -28,9 +32,9 @@
{% if is_filtered %} {% if is_filtered %}
<div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div> <div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div>
{% else %} {% else %}
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body }}</div> <div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
{% endif %} {% endif %}
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap() }}{%- endfor %} <blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
</blockquote> </blockquote>
</details> </details>
</div> </div>

View File

@ -37,7 +37,7 @@
<p class="post_header"> <p class="post_header">
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a> <a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
<a class="post_author" href="/user/{{ post.author.name }}">u/{{ post.author.name }}</a> <a class="post_author {{ post.author.distinguished }}" href="/user/{{ post.author.name }}">u/{{ post.author.name }}</a>
{% if post.author.flair.flair_parts.len() > 0 %} {% if post.author.flair.flair_parts.len() > 0 %}
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small> <small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
{% endif %} {% endif %}
@ -55,15 +55,15 @@
</span> </span>
{% endif %} {% endif %}
</p> </p>
<p class="post_title"> <h1 class="post_title">
<a href="{{ post.permalink }}">{{ post.title }}</a> {{ post.title }}
{% if post.flair.flair_parts.len() > 0 %} {% if post.flair.flair_parts.len() > 0 %}
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on" <a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
class="post_flair" class="post_flair"
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call utils::render_flair(post.flair.flair_parts) %}</a> style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call utils::render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} {% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
</p> </h1>
<!-- POST MEDIA --> <!-- POST MEDIA -->
<!-- post_type: {{ post.post_type }} --> <!-- post_type: {{ post.post_type }} -->
@ -110,7 +110,7 @@
{% endif %} {% endif %}
<!-- POST BODY --> <!-- POST BODY -->
<div class="post_body">{{ post.body }}</div> <div class="post_body">{{ post.body|safe }}</div>
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div> <div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
<div class="post_footer"> <div class="post_footer">
<ul id="post_links"> <ul id="post_links">
@ -144,7 +144,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{{ c.render().unwrap() }} {{ c.render().unwrap()|safe }}
</div> </div>
{%- endfor %} {%- endfor %}

View File

@ -39,7 +39,7 @@
{% endif %} {% endif %}
{% for subreddit in subreddits %} {% for subreddit in subreddits %}
<a href="{{ subreddit.url }}" class="search_subreddit"> <a href="{{ subreddit.url }}" class="search_subreddit">
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div> <div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon|safe }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
<div class="search_subreddit_right"> <div class="search_subreddit_right">
<p class="search_subreddit_header"> <p class="search_subreddit_header">
<span class="search_subreddit_name">r/{{ subreddit.name }}</span> <span class="search_subreddit_name">r/{{ subreddit.name }}</span>
@ -92,13 +92,13 @@
{% if params.before != "" %} {% if params.before != "" %}
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }} <a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }} &sort={{ params.sort }}&t={{ params.t }}
&before={{ params.before }}">PREV</a> &before={{ params.before }}" accesskey="P">PREV</a>
{% endif %} {% endif %}
{% if params.after != "" %} {% if params.after != "" %}
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }} <a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }} &sort={{ params.sort }}&t={{ params.t }}
&after={{ params.after }}">NEXT</a> &after={{ params.after }}" accesskey="N">NEXT</a>
{% endif %} {% endif %}
</footer> </footer>
{% endif %} {% endif %}

View File

@ -11,14 +11,14 @@
<div id="settings"> <div id="settings">
<form action="/settings" method="POST"> <form action="/settings" method="POST">
<div class="prefs"> <div class="prefs">
<p>Appearance</p> <legend>Appearance</legend>
<div id="theme"> <div id="theme">
<label for="theme">Theme:</label> <label for="theme">Theme:</label>
<select name="theme"> <select name="theme">
{% call utils::options(prefs.theme, ["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox"], "system") %} {% call utils::options(prefs.theme, prefs.available_themes, "system") %}
</select> </select>
</div> </div>
<p>Interface</p> <legend>Interface</legend>
<div id="front_page"> <div id="front_page">
<label for="front_page">Front page:</label> <label for="front_page">Front page:</label>
<select name="front_page"> <select name="front_page">
@ -36,7 +36,7 @@
<input type="hidden" value="off" name="wide"> <input type="hidden" value="off" name="wide">
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}> <input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
</div> </div>
<p>Content</p> <legend>Content</legend>
<div id="post_sort"> <div id="post_sort">
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label> <label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
<select name="post_sort"> <select name="post_sort">
@ -79,7 +79,7 @@
</form> </form>
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs"> <div class="prefs" id="settings_subs">
<p>Subscribed Feeds</p> <legend>Subscribed Feeds</legend>
{% for sub in prefs.subscriptions %} {% for sub in prefs.subscriptions %}
<div> <div>
{% let feed -%} {% let feed -%}
@ -94,7 +94,7 @@
{% endif %} {% endif %}
{% if !prefs.filters.is_empty() %} {% if !prefs.filters.is_empty() %}
<div class="prefs" id="settings_filters"> <div class="prefs" id="settings_filters">
<p>Filtered Feeds</p> <legend>Filtered Feeds</legend>
{% for sub in prefs.filters %} {% for sub in prefs.filters %}
<div> <div>
{% let feed -%} {% let feed -%}
@ -110,7 +110,7 @@
<div id="settings_note"> <div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br> <p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p> <p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&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 }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
</form> </form>
{% if sub.name.contains("+") %} {% if sub.name.contains("+") %}
<form action="/r/{{ sub.name }}/subscribe" method="POST"> <form action="/r/{{ sub.name }}/subscribe?redirect={{ redirect_url }}" method="POST">
<button id="multisub" class="subscribe" title="Subscribe to each sub in this multireddit">Subscribe to Multireddit</button> <button id="multisub" class="subscribe" title="Subscribe to each sub in this multireddit">Subscribe to Multireddit</button>
</form> </form>
{% endif %} {% endif %}
@ -64,22 +64,22 @@
{% endif %} {% endif %}
<footer> <footer>
{% if ends.0 != "" %} {% if !ends.0.is_empty() %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
{% endif %} {% endif %}
{% if ends.1 != "" %} {% if !ends.1.is_empty() %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
{% endif %} {% endif %}
</footer> </footer>
</div> </div>
{% endif %} {% endif %}
{% if is_filtered || (sub.name != "" && !sub.name.contains("+")) %} {% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %}
<aside> <aside>
{% if is_filtered %} {% if is_filtered %}
<center>(Content from r/{{ sub.name }} has been filtered)</center> <center>(Content from r/{{ sub.name }} has been filtered)</center>
{% endif %} {% endif %}
{% if sub.name != "" && !sub.name.contains("+") %} {% if !sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+") %}
<div class="panel" id="subreddit"> <div class="panel" id="subreddit">
{% if sub.wiki %} {% if sub.wiki %}
<div id="top"> <div id="top">
@ -89,7 +89,7 @@
{% endif %} {% endif %}
<div id="sub_meta"> <div id="sub_meta">
<img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}"> <img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}">
<p id="sub_title">{{ sub.title }}</p> <h1 id="sub_title">{{ sub.title }}</h1>
<p id="sub_name">r/{{ sub.name }}</p> <p id="sub_name">r/{{ sub.name }}</p>
<p id="sub_description">{{ sub.description }}</p> <p id="sub_description">{{ sub.description }}</p>
<div id="sub_details"> <div id="sub_details">
@ -101,22 +101,22 @@
<div id="sub_actions"> <div id="sub_actions">
<div id="sub_subscription"> <div id="sub_subscription">
{% if prefs.subscriptions.contains(sub.name) %} {% if prefs.subscriptions.contains(sub.name) %}
<form action="/r/{{ sub.name }}/unsubscribe" method="POST"> <form action="/r/{{ sub.name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
<button class="unsubscribe">Unsubscribe</button> <button class="unsubscribe">Unsubscribe</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ sub.name }}/subscribe" method="POST"> <form action="/r/{{ sub.name }}/subscribe?redirect={{ redirect_url }}" method="POST">
<button class="subscribe">Subscribe</button> <button class="subscribe">Subscribe</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
<div id="sub_filter"> <div id="sub_filter">
{% if prefs.filters.contains(sub.name) %} {% if prefs.filters.contains(sub.name) %}
<form action="/r/{{ sub.name }}/unfilter" method="POST"> <form action="/r/{{ sub.name }}/unfilter?redirect={{ redirect_url }}" method="POST">
<button class="unfilter">Unfilter</button> <button class="unfilter">Unfilter</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ sub.name }}/filter" method="POST"> <form action="/r/{{ sub.name }}/filter?redirect={{ redirect_url }}" method="POST">
<button class="filter">Filter</button> <button class="filter">Filter</button>
</form> </form>
{% endif %} {% endif %}
@ -127,7 +127,7 @@
<details class="panel" id="sidebar"> <details class="panel" id="sidebar">
<summary id="sidebar_label">Sidebar</summary> <summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents"> <div id="sidebar_contents">
{{ sub.info }} {{ sub.info|safe }}
{# <hr> {# <hr>
<h2>Moderators</h2> <h2>Moderators</h2>
<br> <br>

View File

@ -16,9 +16,12 @@
{% if !is_filtered %} {% if !is_filtered %}
<div id="column_one"> <div id="column_one">
<form id="sort"> <form id="sort">
<select name="sort"> <div id="listing_options">
{% call utils::options(sort.0, ["hot", "new", "top"], "") %} {% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t"> </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") %} {% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<button id="sort_submit" class="submit"> </select>{% endif %}<button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round"> <svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
@ -49,7 +52,7 @@
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a> <a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span> <span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
</summary> </summary>
<p class="comment_body">{{ post.body }}</p> <p class="comment_body">{{ post.body|safe }}</p>
</details> </details>
</div> </div>
{% endif %} {% endif %}
@ -63,11 +66,11 @@
<footer> <footer>
{% if ends.0 != "" %} {% if ends.0 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
{% endif %} {% endif %}
{% if ends.1 != "" %} {% if ends.1 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a> <a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
{% endif %} {% endif %}
</footer> </footer>
</div> </div>
@ -78,7 +81,7 @@
{% endif %} {% endif %}
<div class="panel" id="user"> <div class="panel" id="user">
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon"> <img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
<p id="user_title">{{ user.title }}</p> <h1 id="user_title">{{ user.title }}</h1>
<p id="user_name">u/{{ user.name }}</p> <p id="user_name">u/{{ user.name }}</p>
<div id="user_description">{{ user.description }}</div> <div id="user_description">{{ user.description }}</div>
<div id="user_details"> <div id="user_details">
@ -91,22 +94,22 @@
{% let name = ["u_", user.name.as_str()].join("") %} {% let name = ["u_", user.name.as_str()].join("") %}
<div id="user_subscription"> <div id="user_subscription">
{% if prefs.subscriptions.contains(name) %} {% if prefs.subscriptions.contains(name) %}
<form action="/r/{{ name }}/unsubscribe" method="POST"> <form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
<button class="unsubscribe">Unfollow</button> <button class="unsubscribe">Unfollow</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ name }}/subscribe" method="POST"> <form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
<button class="subscribe">Follow</button> <button class="subscribe">Follow</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
<div id="user_filter"> <div id="user_filter">
{% if prefs.filters.contains(name) %} {% if prefs.filters.contains(name) %}
<form action="/r/{{ name }}/unfilter" method="POST"> <form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST">
<button class="unfilter">Unfilter</button> <button class="unfilter">Unfilter</button>
</form> </form>
{% else %} {% else %}
<form action="/r/{{ name }}/filter" method="POST"> <form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
<button class="filter">Filter</button> <button class="filter">Filter</button>
</form> </form>
{% endif %} {% endif %}

View File

@ -1,6 +1,6 @@
{% macro options(current, values, default) -%} {% macro options(current, values, default) -%}
{% for value in values %} {% for value in values %}
<option value="{{ value }}" {% if current == value || (current == "" && value == default) %}selected{% endif %}> <option value="{{ value }}" {% if current == value.to_string() || (current == "" && value.to_string() == default.to_string()) %}selected{% endif %}>
{{ format!("{}{}", value.get(0..1).unwrap_or_default().to_uppercase(), value.get(1..).unwrap_or_default()) }} {{ format!("{}{}", value.get(0..1).unwrap_or_default().to_uppercase(), value.get(1..).unwrap_or_default()) }}
</option> </option>
{% endfor %} {% endfor %}
@ -8,7 +8,7 @@
{% macro sort(root, methods, selected) -%} {% macro sort(root, methods, selected) -%}
{% for method in methods %} {% for method in methods %}
<a {% if method == selected %}class="selected"{% endif %} href="{{ root }}/{{ method }}"> <a {% if method.to_string() == selected.to_string() %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
{{ format!("{}{}", method.get(0..1).unwrap_or_default().to_uppercase(), method.get(1..).unwrap_or_default()) }} {{ format!("{}{}", method.get(0..1).unwrap_or_default().to_uppercase(), method.get(1..).unwrap_or_default()) }}
</a> </a>
{% endfor %} {% endfor %}
@ -19,7 +19,7 @@
<input id="search" type="text" name="q" placeholder="Search" title="Search libreddit" value="{{ search }}"> <input id="search" type="text" name="q" placeholder="Search" title="Search libreddit" value="{{ search }}">
{% if root != "/r/" && !root.is_empty() %} {% if root != "/r/" && !root.is_empty() %}
<div id="inside"> <div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr"> <input type="checkbox" name="restrict_sr" id="restrict_sr" checked>
<label for="restrict_sr" class="search_label" title="Restrict search to this subreddit">in {{ root }}</label> <label for="restrict_sr" class="search_label" title="Restrict search to this subreddit">in {{ root }}</label>
</div> </div>
{% endif %} {% endif %}
@ -34,11 +34,10 @@
{%- endmacro %} {%- endmacro %}
{% macro render_flair(flair_parts) -%} {% macro render_flair(flair_parts) -%}
{% for flair_part in flair_parts %}{% if flair_part.flair_part_type == "emoji" %}<span class="emoji" style="background-image:url('{{ flair_part.value }}');"></span>{% else if flair_part.flair_part_type == "text" && !flair_part.value.is_empty() %}<span>{{ flair_part.value }}</span>{% endif %}{% endfor %} {% for flair_part in flair_parts.clone() %}{% if flair_part.flair_part_type == "emoji" %}<span class="emoji" style="background-image:url('{{ flair_part.value }}');"></span>{% else if flair_part.flair_part_type == "text" && !flair_part.value.is_empty() %}<span>{{ flair_part.value }}</span>{% endif %}{% endfor %}
{%- endmacro %} {%- endmacro %}
{% macro sub_list(current) -%} {% macro sub_list(current) -%}
{% if prefs.subscriptions.len() > 0 %}
<details id="feeds"> <details id="feeds">
<summary>Feeds</summary> <summary>Feeds</summary>
<div id="feed_list"> <div id="feed_list">
@ -46,13 +45,14 @@
<a href="/">Home</a> <a href="/">Home</a>
<a href="/r/popular">Popular</a> <a href="/r/popular">Popular</a>
<a href="/r/all">All</a> <a href="/r/all">All</a>
<p>REDDIT FEEDS</p> {% if prefs.subscriptions.len() > 0 %}
{% for sub in prefs.subscriptions %} <p>REDDIT FEEDS</p>
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a> {% for sub in prefs.subscriptions %}
{% endfor %} <a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a>
{% endfor %}
{% endif %}
</div> </div>
</details> </details>
{% endif %}
{%- endmacro %} {%- endmacro %}
{% macro render_hls_notification(redirect_url) -%} {% macro render_hls_notification(redirect_url) -%}
@ -72,7 +72,7 @@
{% endif -%} {% endif -%}
<a class="post_subreddit" href="/{{ community }}">{{ community }}</a> <a class="post_subreddit" href="/{{ community }}">{{ community }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a> <a class="post_author {{ post.author.distinguished }}" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
<span class="dot">&bull;</span> <span class="dot">&bull;</span>
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span> <span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
{% if !post.awards.is_empty() %} {% if !post.awards.is_empty() %}
@ -83,7 +83,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</p> </p>
<p class="post_title"> <h2 class="post_title">
{% if post.flair.flair_parts.len() > 0 %} {% if post.flair.flair_parts.len() > 0 %}
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on" <a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
class="post_flair" class="post_flair"
@ -91,7 +91,7 @@
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a> dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
{% endif %} {% endif %}
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %} <a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
</p> </h2>
<!-- POST MEDIA/THUMBNAIL --> <!-- POST MEDIA/THUMBNAIL -->
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %} {% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height / post.media.width < 2 %}short{% endif %}" > <a href="{{ post.media.url }}" class="post_media_image {% if post.media.height / post.media.width < 2 %}short{% endif %}" >
@ -138,10 +138,10 @@
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div> <div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
<div class="post_body post_preview"> <div class="post_body post_preview">
{{ post.body }} {{ post.body|safe }}
</div> </div>
<div class="post_footer"> <div class="post_footer">
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} comments">{{ post.comments.0 }} comments</a> <a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}">{{ post.comments.0 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}</a>
</div> </div>
</div> </div>
{%- endmacro %} {%- endmacro %}

View File

@ -22,8 +22,8 @@
<div>Wiki</div> <div>Wiki</div>
</div> </div>
<div id="wiki"> <div id="wiki">
{{ wiki }} {{ wiki|safe }}
</div> </div>
</div> </div>
</main> </main>
{% endblock %} {% endblock %}