Compare commits

...

80 Commits

Author SHA1 Message Date
1d4ea50a45 Add setting to autoplay videos 2021-10-25 21:27:55 -07:00
546c8a4cda Add poster attribute and disable autoplay on GIFs
* Add the poster attribute even if a post claims to be type gif. Default to none-preloading for gifs like video-typed posts do.

* Disable autoplay for videos in feeds

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2021-10-23 21:25:51 +00:00
03336ecafd Add libreddit.de and onion host. Closes #300 2021-10-19 13:59:46 -07:00
957e1c7728 Add oversold.host instance. Closes #289 2021-10-08 23:28:21 +00:00
09053ef0ad Add drivet.xyz instance. Closes #287 2021-10-07 00:13:48 +00:00
aff030fc3a Remove bcow instances 2021-10-02 05:12:49 +00:00
97555dbfdd Fix silkky.cloud Cloudflare Status (#284) 2021-09-29 00:56:09 +00:00
32360e5165 Update v0.15.3 2021-09-19 14:37:31 -07:00
350b796571 Support Deployment to Heroku (#280)
* Added heroku.yml

* Added app.json

* PORT as env var
2021-09-19 19:03:01 +00:00
567556711b Update style.css (#282)
Add cursor pointer on hover of summary bar
2021-09-19 19:00:07 +00:00
1ff725ba2e Add flux.industries instance. Closes #279 2021-09-11 19:46:50 +00:00
6a4191f3b5 Fix #272 2021-09-09 17:28:55 -07:00
668493b72c Add autarkic.org instance. Closes #274 2021-09-10 00:18:03 +00:00
db04dcb238 Add igna.rock instance. Closes #276 2021-09-10 00:16:46 +00:00
cc0a1e0324 Use proper spelling of systemd (#275) 2021-09-10 00:15:09 +00:00
e073fc87aa Add alefvanoon.xyz (#271) 2021-09-08 14:28:53 -07:00
982f57efd9 Fix user profiles showing up in search engines 2021-09-06 12:05:03 -07:00
52a1b45014 Lazy load images 2021-09-06 12:02:52 -07:00
6f88fdfc75 Add mint.lgbt instance. Closes #269 2021-09-04 20:07:42 +00:00
015d0b3414 Add stuehieyr.com instance. Closes #267 2021-09-01 00:20:11 +00:00
b41eabecf7 Remove instance (#266) 2021-08-29 18:41:30 +00:00
5cb5f46fa2 Add new instance (libreddit.sugoma.tk) (#262) 2021-08-21 16:52:39 +00:00
a900339529 Add some-things.org instances. Closes #260 2021-08-18 17:06:04 +00:00
41b3dc5739 More apt error messages for Reddit outages 2021-08-11 20:49:42 -07:00
b3b5782373 Handle Docker amd64 builds in GitHub Actions 2021-08-04 12:08:25 -07:00
5c753ee171 Fix #251 2021-08-04 11:52:24 -07:00
229518c40b Remove cyberhost instance. Closes #252 2021-08-01 16:48:26 +00:00
45a5778571 Escape text-only flairs 2021-07-19 10:20:00 -07:00
be253d40dd Escape html characters in post flairs (#247)
* Encode HTML characters in flairs

* Encode HTML characters in flairs

* Use esc! macro for HTML escaping

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2021-07-19 17:15:15 +00:00
e571cc3b1e Tweak styling of Dracula theme 2021-07-19 10:07:00 -07:00
345f8e7b80 Dampen title color of visited posts. Fixes #222 2021-07-18 14:53:08 -07:00
a190890239 Fix Regex matching of Reddit links 2021-07-17 22:24:28 -07:00
ee51ce1a76 Add instance (#242) 2021-07-07 00:08:02 +00:00
81a2df98cb Add awesomehub.io instance. Closes #239 2021-07-05 00:14:38 +00:00
e79a4b704a Add cyberhost.uk instance. Closes #238 2021-07-05 00:03:38 +00:00
56998b8332 Rewrite redd.it links 2021-06-21 22:51:50 -07:00
5418303b08 ARMV7 Docker Image (#234)
* feat: Push dockerfile.

Progress on trying to get armv7 image.

* feat: Add Dockerfile.

Using rust:slim as builder image.

* refactor: Changes to build for armv7.

* feat: Add .cargo config.

Taken from: https://medium.com/swlh/compiling-rust-for-raspberry-pi-arm-922b55dbb050

* refactor: Add environment variable for linker.

Instead of .cargo config file.

* feat: Working cross compile version.

For Armv7.

* refactor: Clean up dockerfile.

* refactor: Rename to armv7.

Rename Dockerfile.armv7rust to Dockerfile.armv7.

* feat: Add workflow to build ARMv7 docker image.

* docs: Add armv7 deployment instructions.
2021-06-21 23:03:27 +00:00
5ab41c4e6e Add r.nf instance. Closes #233 2021-06-18 23:46:20 +00:00
807b3ffeca Add artemislena.eu instance. Closes #232 2021-06-17 20:44:42 +00:00
85deb4947d Support HLS playback in search and user feeds 2021-06-11 17:38:43 -07:00
d2002c9027 Disable dysfunctional moderator list feature 2021-06-11 11:03:36 -07:00
f84f4c0326 Add Trevor instance 2021-05-31 04:55:39 +00:00
ca3f6c0579 Fix #228 2021-05-28 12:01:20 -07:00
decc9e5139 Include SystemD configuration (#227) 2021-05-28 04:33:14 +00:00
d27bd782ce Specify fallback fonts 2021-05-26 20:30:08 -07:00
4defb58f2a Optimizations and commenting 2021-05-20 12:24:06 -07:00
ba42fc066f Fix two subscription bugs 2021-05-19 20:30:10 -07:00
2cd35fb3b6 Upgrade to v0.14.2 2021-05-19 16:12:21 -07:00
b9af6f47f3 Use Inter font 2021-05-19 16:09:08 -07:00
73732a2a44 Fix subscription clearing when saving settings 2021-05-19 15:59:32 -07:00
43ed9756dc Upgrade to v0.14 2021-05-16 09:11:38 -07:00
8bb247af3b Added support for quarantined subreddits (#219)
* Added support for quarantined subreddits

* Added confirmation wall for quarantined subreddits

* Added quarantine walls to other routes and fixed case issue

* Correct obsolete use of cookie()

* Refactor param() and quarantine()

Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2021-05-16 15:53:39 +00:00
ed05f5a092 misc: fix HLS typo (#220) 2021-05-16 15:41:47 +00:00
4f09333cd7 Handle three unwraps 2021-05-15 14:51:57 -07:00
31bf8c802e Document server configuration 2021-05-15 14:32:38 -07:00
e4f9bd7b8d Configure default settings using environment variables 2021-05-15 13:59:42 -07:00
83a667347d Add rel="nofollow" to hardcoded outbound links 2021-05-10 10:31:19 -07:00
499a56aed4 Bump version to v0.12.0 2021-05-09 18:27:25 -07:00
928907086c HLS video playback (#182)
* HLS video playback

Signed-off-by: Adrian Lebioda <adrianlebioda@gmail.com>

* Add LibreJS compliance

* Locally host hls.js

* Notification about HLS under videos that support it

Signed-off-by: Adrian Lebioda <adrianlebioda@gmail.com>

* Use .contains() instead of .find() == None

* Make list of preferences constant

* Change headers_keys from Vector into Array

* Fix incorrect detecting of # in paths

* Remove trailing-slash-appending if statement

* Change HLS notification styling

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2021-05-10 01:25:52 +00:00
dc9fbc1a05 Don't upload Libreddit releases in pull requests 2021-05-09 18:13:24 -07:00
7ae7a88eed Upgrade to v0.11.2 2021-05-09 08:51:01 -07:00
536a766960 WIP: Various subreddit & post fixes (#215)
* Fixed random subreddit issue

* Fixed large subreddit icon rendering

* Formatting fix

* Fix dodgy HTML rendering issues

* Revert "Fix dodgy HTML rendering issues"

This reverts commit 58be5f449b72f271d2b3c046870b652d1e715289.
2021-05-09 15:40:49 +00:00
e34329cfee Upgrade to v0.11.1 2021-05-08 22:09:47 -07:00
97a0680bd0 Support GIFs in comments (#217)
* Support GIFs in comments

* Fix removing Giphy links so it only removes Giphy links

* Remove removing link to Giphy
2021-05-09 01:22:26 +00:00
c1560f4eba Upgrade to v0.11.0 2021-05-06 12:31:59 -07:00
242ffab0da Fix bug with subreddit subscription case & RTL languages (#214)
* Fixed subreddit subscription case issues

* Fixed formatting

* Fixed flair RTL language issue (#132)

* Convert display_lookup to Vec

Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
2021-05-06 19:11:25 +00:00
1211d781d0 Add list of moderators to sidebar (#213)
* Added list of moderators to sidebar & added wiki not found message

* Improved code formatting
2021-05-04 17:30:54 +00:00
9e4066658c Added 2 new themes: violet & gold (#212)
* Added 2 new themes: violet & gold

* Increased contrast in Violet theme

* Changed accent colour on violet theme
2021-05-03 16:48:21 +00:00
560de4e91f removed himiko.cloud instances (#211) 2021-05-02 19:26:13 +00:00
bd1c890961 Update to v0.10.6 2021-04-30 16:35:41 +00:00
6f799b2617 Added laserwave theme (#210) 2021-04-30 16:26:49 +00:00
38e176f59f Add riverside.rocks instance (#209) 2021-04-30 16:25:50 +00:00
8248eca95c Correct Actix → Hyper in Readme 2021-04-27 18:54:38 +00:00
ffc3bfe72d Add libreddit.domain.glass instance 2021-04-22 19:26:42 +00:00
d713746407 doc: add new self-hosted hidden service => http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion/ (#203) 2021-04-22 19:14:11 +00:00
21b45760eb Add exonip.de instance. Closes #200 2021-04-20 16:41:29 +00:00
e3fb93946a Revert ARM builds to only arm64 2021-04-17 21:32:48 -07:00
b6134a39d0 Specify build platform for Docker ARM builds 2021-04-17 21:11:33 -07:00
c844655c98 Use rust:latest as base Docker image for arm tag 2021-04-17 20:56:12 -07:00
cac83493da Add arm/v7 platform for Docker builds 2021-04-17 18:58:18 -07:00
33 changed files with 1399 additions and 620 deletions

View File

@ -30,7 +30,7 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./Dockerfile.arm64 file: ./Dockerfile.arm
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: spikecodes/libreddit:arm tags: spikecodes/libreddit:arm

39
.github/workflows/docker-armv7.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Docker ARM V7 Build
on:
push:
paths-ignore:
- "**.md"
branches:
- master
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
id: build_push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.armv7
platforms: linux/arm/v7
push: true
tags: spikecodes/libreddit:armv7

37
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Docker amd64 Build
on:
push:
paths-ignore:
- "**.md"
branches:
- master
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: spikecodes/libreddit:latest

View File

@ -39,6 +39,7 @@ jobs:
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
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 }} - NAME
@ -48,7 +49,5 @@ jobs:
libreddit.sha512 libreddit.sha512
body: | body: |
- ${{ github.event.head_commit.message }} ${{ github.sha }} - ${{ github.event.head_commit.message }} ${{ github.sha }}
See full list of changes [here](https://github.com/spikecodes/libreddit/compare/${{ steps.version.outputs.tag }}...${{ steps.version.outputs.version }}).
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

389
Cargo.lock generated
View File

@ -1,10 +1,12 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -77,10 +79,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "async-trait" name = "async-rwlock"
version = "0.1.49" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589652ce7ccb335d1e7ecb3be145425702b290dbcb7029bbeaae263fc1d87b48" checksum = "261803dcc39ba9e72760ba6e16d0199b1eef9fc44e81bffabbebb9f5aea3906c"
dependencies = [
"async-mutex",
"event-listener",
]
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -107,9 +119,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitvec" name = "bitvec"
@ -125,23 +137,24 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.6.1" version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.0.1" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]] [[package]]
name = "cached" name = "cached"
version = "0.23.0" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2afe73808fbaac302e39c9754bfc3c4b4d0f99c9c240b9f4e4efc841ad1b74" checksum = "c2bc2fd249a24a9cdd4276f3a3e0461713271ab63b0e9e656e200e8e21c8c927"
dependencies = [ dependencies = [
"async-mutex", "async-mutex",
"async-rwlock",
"async-trait", "async-trait",
"cached_proc_macro", "cached_proc_macro",
"cached_proc_macro_types", "cached_proc_macro_types",
@ -152,11 +165,10 @@ dependencies = [
[[package]] [[package]]
name = "cached_proc_macro" name = "cached_proc_macro"
version = "0.6.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf857ae42d910aede5c5186e62684b0d7a597ce2fe3bd14448ab8f7ef439848c" checksum = "ac3531903b39df48a378a7ed515baee7c1fff32488489c7d0725eb1749b22a91"
dependencies = [ dependencies = [
"async-mutex",
"cached_proc_macro_types", "cached_proc_macro_types",
"darling", "darling",
"quote", "quote",
@ -171,9 +183,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.67" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -194,15 +206,15 @@ dependencies = [
[[package]] [[package]]
name = "const_fn" name = "const_fn"
version = "0.4.6" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.15.0" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627" checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
dependencies = [ dependencies = [
"time", "time",
"version_check", "version_check",
@ -210,9 +222,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -220,9 +232,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "ct-logs" name = "ct-logs"
@ -235,9 +247,9 @@ dependencies = [
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.10.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"darling_macro", "darling_macro",
@ -245,9 +257,9 @@ dependencies = [
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.10.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3"
dependencies = [ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
@ -259,9 +271,9 @@ dependencies = [
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.10.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
@ -282,9 +294,9 @@ checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
dependencies = [ dependencies = [
"instant", "instant",
] ]
@ -313,9 +325,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253" checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -328,9 +340,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25" checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -338,15 +350,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f6cb7042eda00f0049b1d2080aa4b93442997ee507eb3828e8bd7577f94c9d" checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -355,15 +367,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04" checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "1.11.3" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb" checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"futures-core", "futures-core",
@ -376,10 +388,11 @@ dependencies = [
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668c6733a182cd7deb4f1de7ba3bf2120823835b3bcfbeacf7d2c4a773c1bb8b" checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
dependencies = [ dependencies = [
"autocfg",
"proc-macro-hack", "proc-macro-hack",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -388,22 +401,23 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23" checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.14" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [ dependencies = [
"autocfg",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
@ -420,9 +434,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.2" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -439,24 +453,24 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.9.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -465,9 +479,9 @@ dependencies = [
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.1" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfb77c123b4e2f72a2069aeae0b4b4949cc7e966df277813fc16347e7549737" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@ -476,21 +490,21 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.4.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
version = "0.3.2" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.5" version = "0.14.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -502,7 +516,7 @@ dependencies = [
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
"tower-service", "tower-service",
@ -546,9 +560,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.6.2" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -556,24 +570,24 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.50" version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -586,9 +600,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lexical-core" name = "lexical-core"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags", "bitflags",
@ -599,13 +613,13 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.93" version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
[[package]] [[package]]
name = "libreddit" name = "libreddit"
version = "0.10.5" version = "0.16.0"
dependencies = [ dependencies = [
"askama", "askama",
"async-recursion", "async-recursion",
@ -626,9 +640,9 @@ dependencies = [
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.3" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
] ]
@ -644,21 +658,21 @@ dependencies = [
[[package]] [[package]]
name = "matches" name = "matches"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.7.11" version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@ -710,15 +724,15 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.7.2" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.2" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]] [[package]]
name = "parking" name = "parking"
@ -728,9 +742,9 @@ checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
@ -739,9 +753,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"instant", "instant",
@ -757,31 +771,11 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -803,18 +797,18 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.26" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -827,18 +821,18 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.6" version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.5" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -847,9 +841,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.23" version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]] [[package]]
name = "ring" name = "ring"
@ -868,9 +862,9 @@ dependencies = [
[[package]] [[package]]
name = "route-recognizer" name = "route-recognizer"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824172f0afccf3773c3905f5550ac94572144efe0deaf49a1f22bbca188d193e" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
@ -883,9 +877,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.19.0" version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [ dependencies = [
"base64", "base64",
"log", "log",
@ -940,9 +934,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.2.0" version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@ -953,9 +947,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.2.0" version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -978,18 +972,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.125" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.125" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -998,9 +992,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.64" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1015,30 +1009,30 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.2" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.6.1" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.0" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [ dependencies = [
"libc", "libc",
"winapi", "winapi",
@ -1116,15 +1110,15 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.9.3" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.69" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1148,9 +1142,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.2.26" version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [ dependencies = [
"const_fn", "const_fn",
"libc", "libc",
@ -1173,9 +1167,9 @@ dependencies = [
[[package]] [[package]]
name = "time-macros-impl" name = "time-macros-impl"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
dependencies = [ dependencies = [
"proc-macro-hack", "proc-macro-hack",
"proc-macro2", "proc-macro2",
@ -1186,9 +1180,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.2.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -1201,9 +1195,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.5.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1221,9 +1215,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.1.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1243,9 +1237,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.6.6" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@ -1263,9 +1257,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.25" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"pin-project-lite", "pin-project-lite",
@ -1274,9 +1268,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.17" version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
] ]
@ -1289,33 +1283,30 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.5" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
dependencies = [
"matches",
]
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.17" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
@ -1325,9 +1316,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@ -1359,9 +1350,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.73" version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -1369,9 +1360,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.73" version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@ -1384,9 +1375,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.73" version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -1394,9 +1385,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.73" version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1407,15 +1398,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.73" version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.50" version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@ -3,23 +3,23 @@ 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.10.5" version = "0.16.0"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
askama = { version = "0.10.5", default-features = false } askama = { version = "0.10.5", default-features = false }
async-recursion = "0.3.2" async-recursion = "0.3.2"
cached = "0.23.0" cached = "0.26.2"
clap = { version = "2.33.3", default-features = false } clap = { version = "2.33.3", default-features = false }
regex = "1.4.5" regex = "1.5.4"
serde = { version = "1.0.125", features = ["derive"] } serde = { version = "1.0.130", features = ["derive"] }
cookie = "0.15.0" cookie = "0.15.1"
futures-lite = "1.11.3" futures-lite = "1.12.0"
hyper = { version = "0.14.5", features = ["full"] } hyper = { version = "0.14.14", features = ["full"] }
hyper-rustls = "0.22.1" hyper-rustls = "0.22.1"
route-recognizer = "0.3.0" route-recognizer = "0.3.1"
serde_json = "1.0.64" serde_json = "1.0.68"
tokio = { version = "1.5.0", features = ["full"] } tokio = { version = "1.12.0", features = ["full"] }
time = "0.2.26" time = "0.2.7"
url = "2.2.1" url = "2.2.2"

43
Dockerfile.armv7 Normal file
View File

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

View File

@ -1,6 +1,6 @@
# Libreddit # Libreddit
> An alternative private front-end to Reddit > An alternative private front-end to Reddit
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png) ![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png)
@ -31,17 +31,34 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
| [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) | 🇺🇸 US | | | [libreddit.dothq.co](https://libreddit.dothq.co) | 🇺🇸 US | |
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ | | [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ |
| [libreddit.himiko.cloud](https://libreddit.himiko.cloud) | 🇫🇮 FI | |
| [libreddit.bcow.xyz](https://libreddit.bcow.xyz) | 🇺🇸 US | |
| [libreddit.40two.app](https://libreddit.40two.app) | 🇳🇱 NL | | | [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 | | | [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | | | [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | ✅ |
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ | | [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ |
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | | | [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | | | [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | |
| [libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion](http://libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion) | 🇫🇮 FI | | | [libreddit.sugoma.tk](https://libreddit.sugoma.tk) | 🇺🇸 US | |
| [dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion](http://dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion/) | 🇺🇸 US | | | [libreddit.trevorthalacker.com](https://libreddit.trevorthalacker.com) | 🇺🇸 US | |
| [reddit.artemislena.eu](https://reddit.artemislena.eu) | 🇩🇪 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 | |
| [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | |
| [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | |
| [libreddit.alefvanoon.xyz](https://libreddit.alefvanoon.xyz) | 🇺🇸 US | ✅ |
| [libreddit.igna.rocks](https://libreddit.igna.rocks) | 🇺🇸 US | |
| [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 | |
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
| [inytumdgnri7xsqtvpntjevaelxtgbjqkuqhtf6txxhwbll2fwqtakqd.onion](http://inytumdgnri7xsqtvpntjevaelxtgbjqkuqhtf6txxhwbll2fwqtakqd.onion) | 🇨🇭 CH | |
| [liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion](http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion) | 🇩🇪 DE | |
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. 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.
@ -69,7 +86,7 @@ Teddit is another awesome open source project designed to provide an alternative
If you are looking to compare, the biggest differences I have noticed are: If you are looking to compare, the biggest differences I have noticed are:
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective. - Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db). - Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
--- ---
@ -160,6 +177,8 @@ docker run -d --name libreddit -p 80:8080 spikecodes/libreddit
To deploy on `arm64` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:arm`. To deploy on `arm64` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:arm`.
To deploy on `armv7` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:armv7`.
## 3) AUR ## 3) AUR
For ArchLinux users, Libreddit is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git). For ArchLinux users, Libreddit is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
@ -182,6 +201,10 @@ If you're on Linux and none of these methods work for you, you can grab a Linux
In the web preview (defaults to top right), you should see your instance hosted where you can assign a [custom domain](https://docs.replit.com/repls/web-hosting#custom-domains). In the web preview (defaults to top right), you should see your instance hosted where you can assign a [custom domain](https://docs.replit.com/repls/web-hosting#custom-domains).
## 6) Heroku
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/spikecodes/libreddit)
--- ---
# Deployment # Deployment
@ -192,6 +215,32 @@ Once installed, deploy Libreddit to `0.0.0.0:8080` by running:
libreddit libreddit
``` ```
## Change Default Settings
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 |
|-------------------------|------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
### Examples
```bash
LIBREDDIT_DEFAULT_SHOW_NSFW=on libreddit
```
```bash
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 a NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
@ -200,6 +249,25 @@ proxy_http_version 1.1;
``` ```
to your NGINX configuration file above your `proxy_pass` line. to your NGINX configuration file above your `proxy_pass` line.
## systemd
You can use the systemd service available in `contrib/libreddit.service`
(install it on `/etc/systemd/system/libreddit.service`).
That service can be optionally configured in terms of environment variables by
creating a file in `/etc/libreddit.conf`. Use the `contrib/libreddit.conf` as a
template. You can also add the `LIBREDDIT_DEFAULT__{X}` settings explained
above.
When "Proxying using NGINX" where the proxy is on the same machine, you should
guarantee nginx waits for this service to start. Edit
`/etc/systemd/system/libreddit.service.d/reverse-proxy.conf`:
```conf
[Unit]
Before=nginx.service
```
## Building ## Building
``` ```

42
app.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "Libreddit",
"description": "Private front-end for Reddit",
"buildpacks": [
{
"url": "https://github.com/emk/heroku-buildpack-rust"
},
{
"url": "emk/rust"
}
],
"stack": "container",
"env": {
"LIBREDDIT_DEFAULT_THEME": {
"required": false
},
"LIBREDDIT_DEFAULT_FRONT_PAGE": {
"required": false
},
"LIBREDDIT_DEFAULT_LAYOUT": {
"required": false
},
"LIBREDDIT_DEFAULT_WIDE": {
"required": false
},
"LIBREDDIT_DEFAULT_COMMENT_SORT": {
"required": false
},
"LIBREDDIT_DEFAULT_POST_SORT": {
"required": false
},
"LIBREDDIT_DEFAULT_SHOW_NSFW": {
"required": false
},
"LIBREDDIT_USE_HLS": {
"required": false
},
"LIBREDDIT_HIDE_HLS_NOTIFICATION": {
"required": false
}
}
}

2
contrib/libreddit.conf Normal file
View File

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

15
contrib/libreddit.service Normal file
View File

@ -0,0 +1,15 @@
[Unit]
Description=libreddit daemon
After=network.service
[Service]
DynamicUser=yes
# Default Values
Environment=ADDRESS=0.0.0.0
Environment=PORT=8080
# Optional Override
EnvironmentFile=-/etc/libreddit.conf
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
[Install]
WantedBy=default.target

3
heroku.yml Normal file
View File

@ -0,0 +1,3 @@
build:
docker:
web: Dockerfile

View File

@ -9,14 +9,16 @@ use crate::server::RequestExt;
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> { pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default()); let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
// For each parameter in request
for (name, value) in req.params().iter() { for (name, value) in req.params().iter() {
// Fill the parameter value in the url
url = url.replace(&format!("{{{}}}", name), value); url = url.replace(&format!("{{{}}}", name), value);
} }
stream(&url).await stream(&url, &req).await
} }
async fn stream(url: &str) -> 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 url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?;
@ -26,8 +28,19 @@ async fn stream(url: &str) -> 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);
// Copy useful headers from original request
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
if let Some(value) = req.headers().get(key) {
builder = builder.header(key, value);
}
}
let stream_request = builder.body(Body::empty()).map_err(|_| "Couldn't build empty body in stream".to_string())?;
client client
.get(url) .request(stream_request)
.await .await
.map(|mut res| { .map(|mut res| {
let mut rm = |key: &str| res.headers_mut().remove(key); let mut rm = |key: &str| res.headers_mut().remove(key);
@ -40,19 +53,22 @@ async fn stream(url: &str) -> Result<Response<Body>, String> {
rm("x-cdn-client-region"); rm("x-cdn-client-region");
rm("x-cdn-name"); rm("x-cdn-name");
rm("x-cdn-server-region"); rm("x-cdn-server-region");
rm("x-reddit-cdn");
rm("x-reddit-video-features");
res res
}) })
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
fn request(url: String) -> Boxed<Result<Response<Body>, String>> { fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
// Prepare the HTTPS connector. // Prepare the HTTPS connector.
let https = hyper_rustls::HttpsConnector::with_native_roots(); let https = hyper_rustls::HttpsConnector::with_native_roots();
// Build the hyper client from the HTTPS connector. // Construct 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);
// Build request
let builder = Request::builder() let builder = Request::builder()
.method("GET") .method("GET")
.uri(&url) .uri(&url)
@ -61,6 +77,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Accept-Language", "en-US,en;q=0.5") .header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive") .header("Connection", "keep-alive")
.header("Cookie", if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%7D" } else { "" })
.body(Body::empty()); .body(Body::empty());
async move { async move {
@ -75,6 +92,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
.map(|val| val.to_str().unwrap_or_default()) .map(|val| val.to_str().unwrap_or_default())
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
quarantine,
) )
.await .await
} else { } else {
@ -91,7 +109,7 @@ fn request(url: String) -> Boxed<Result<Response<Body>, String>> {
// Make a request to a Reddit API and parse the JSON response // Make a request to a Reddit API and parse the JSON response
#[cached(size = 100, time = 30, result = true)] #[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String) -> Result<Value, String> { pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
// Build Reddit url from path // Build Reddit url from path
let url = format!("https://www.reddit.com{}", path); let url = format!("https://www.reddit.com{}", path);
@ -102,8 +120,10 @@ pub async fn json(path: String) -> Result<Value, String> {
}; };
// Fetch the url... // Fetch the url...
match request(url.clone()).await { match request(url.clone(), quarantine).await {
Ok(response) => { Ok(response) => {
let status = response.status();
// asynchronously aggregate the chunks of the body // asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await { match hyper::body::aggregate(response).await {
Ok(body) => { Ok(body) => {
@ -128,7 +148,13 @@ pub async fn json(path: String) -> Result<Value, String> {
Ok(json) Ok(json)
} }
} }
Err(e) => err("Failed to parse page JSON data", e.to_string()), Err(e) => {
if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string())
} else {
err("Failed to parse page JSON data", e.to_string())
}
}
} }
} }
Err(e) => err("Failed receiving body from Reddit", e.to_string()), Err(e) => err("Failed receiving body from Reddit", e.to_string()),

View File

@ -3,11 +3,10 @@
#![warn(clippy::pedantic, clippy::all)] #![warn(clippy::pedantic, clippy::all)]
#![allow( #![allow(
clippy::needless_pass_by_value, clippy::needless_pass_by_value,
clippy::match_wildcard_for_single_variants,
clippy::cast_possible_truncation, clippy::cast_possible_truncation,
clippy::similar_names,
clippy::cast_possible_wrap, clippy::cast_possible_wrap,
clippy::find_map clippy::manual_find_map,
clippy::unused_async
)] )]
// Reference local files // Reference local files
@ -66,6 +65,16 @@ async fn favicon() -> Result<Response<Body>, String> {
) )
} }
async fn font() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "font/woff2")
.body(include_bytes!("../static/Inter.var.woff2").as_ref().into())
.unwrap_or_default(),
)
}
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> { async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
let mut res = Response::builder() let mut res = Response::builder()
.status(200) .status(200)
@ -87,6 +96,13 @@ async fn main() {
let matches = cli::new("Libreddit") let matches = cli::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::with_name("redirect-https")
.short("r")
.long("redirect-https")
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false),
)
.arg( .arg(
Arg::with_name("address") Arg::with_name("address")
.short("a") .short("a")
@ -105,13 +121,6 @@ async fn main() {
.default_value("8080") .default_value("8080")
.takes_value(true), .takes_value(true),
) )
.arg(
Arg::with_name("redirect-https")
.short("r")
.long("redirect-https")
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
.takes_value(false),
)
.arg( .arg(
Arg::with_name("hsts") Arg::with_name("hsts")
.short("H") .short("H")
@ -124,10 +133,11 @@ async fn main() {
.get_matches(); .get_matches();
let address = matches.value_of("address").unwrap_or("0.0.0.0"); let address = matches.value_of("address").unwrap_or("0.0.0.0");
let port = matches.value_of("port").unwrap_or("8080"); let port = std::env::var("PORT")
.unwrap_or_else(|_| matches.value_of("port").unwrap_or("8080").to_string());
let hsts = matches.value_of("hsts"); let hsts = matches.value_of("hsts");
let listener = format!("{}:{}", address, port); let listener = [address, ":", &port].concat();
println!("Starting Libreddit..."); println!("Starting Libreddit...");
@ -139,7 +149,7 @@ async fn main() {
"Referrer-Policy" => "no-referrer", "Referrer-Policy" => "no-referrer",
"X-Content-Type-Options" => "nosniff", "X-Content-Type-Options" => "nosniff",
"X-Frame-Options" => "DENY", "X-Frame-Options" => "DENY",
"Content-Security-Policy" => "default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';" "Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;"
}; };
if let Some(expire_time) = hsts { if let Some(expire_time) = hsts {
@ -153,14 +163,24 @@ async fn main() {
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());
app.at("/robots.txt").get(|_| resource("User-agent: *\nAllow: /", "text/plain", true).boxed()); app
.at("/robots.txt")
.get(|_| resource("User-agent: *\nDisallow: /u/\nDisallow: /user/", "text/plain", true).boxed());
app.at("/favicon.ico").get(|_| favicon().boxed()); app.at("/favicon.ico").get(|_| favicon().boxed());
app.at("/logo.png").get(|_| pwa_logo().boxed()); app.at("/logo.png").get(|_| pwa_logo().boxed());
app.at("/Inter.var.woff2").get(|_| font().boxed());
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed()); app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed()); app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
app
.at("/playHLSVideo.js")
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
app
.at("/hls.min.js")
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
// Proxy media through Libreddit // Proxy media through Libreddit
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed()); app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
app.at("/hls/:id/*path").get(|r| proxy(r, "https://v.redd.it/{id}/{path}").boxed());
app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed()); app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").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());
@ -184,9 +204,13 @@ async fn main() {
// Configure settings // Configure settings
app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed()); app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed());
app.at("/settings/restore").get(|r| settings::restore(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed());
// Subreddit services // Subreddit services
app.at("/r/:sub").get(|r| subreddit::community(r).boxed()); app
.at("/r/:sub")
.get(|r| subreddit::community(r).boxed())
.post(|r| subreddit::add_quarantine_exception(r).boxed());
app app
.at("/r/u_:name") .at("/r/u_:name")
@ -236,7 +260,7 @@ async fn main() {
app.at("/:id").get(|req: Request<Body>| match req.param("id").as_deref() { app.at("/:id").get(|req: Request<Body>| match req.param("id").as_deref() {
// Sort front page // Sort front page
Some("best") | Some("hot") | Some("new") | Some("top") | Some("rising") | Some("controversial") => subreddit::community(req).boxed(), Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).boxed(),
// Short link for post // Short link for post
Some(id) if id.len() > 4 && id.len() < 7 => post::item(req).boxed(), Some(id) if id.len() > 4 && id.len() < 7 => post::item(req).boxed(),
// Error message for unknown pages // Error message for unknown pages

View File

@ -2,10 +2,10 @@
use crate::client::json; use crate::client::json;
use crate::esc; use crate::esc;
use crate::server::RequestExt; use crate::server::RequestExt;
use crate::utils::{cookie, error, format_num, format_url, param, rewrite_urls, template, time, val, Author, Comment, Flags, Flair, FlairPart, Media, Post, Preferences}; use crate::subreddit::{can_access_quarantine, quarantine};
use hyper::{Body, Request, Response}; use crate::utils::{error, format_num, format_url, param, rewrite_urls, setting, template, time, val, Author, Comment, Flags, Flair, FlairPart, Media, Post, Preferences};
use async_recursion::async_recursion; use hyper::{Body, Request, Response};
use askama::Template; use askama::Template;
@ -23,18 +23,22 @@ struct PostTemplate {
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path // Build Reddit API path
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default()); let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
let sub = req.param("sub").unwrap_or_default();
let quarantined = can_access_quarantine(&req, &sub);
// Set sort to sort query parameter // Set sort to sort query parameter
let mut sort: String = param(&path, "sort"); let sort = param(&path, "sort").unwrap_or_else(|| {
// Grab default comment sort method from Cookies
let default_sort = setting(&req, "comment_sort");
// Grab default comment sort method from Cookies // If there's no sort query but there's a default sort, set sort to default_sort
let default_sort = cookie(&req, "comment_sort"); if default_sort.is_empty() {
String::new()
// If there's no sort query but there's a default sort, set sort to default_sort } else {
if sort.is_empty() && !default_sort.is_empty() { path = format!("{}.json?{}&sort={}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), default_sort);
sort = default_sort; default_sort
path = format!("{}.json?{}&sort={}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), sort); }
} });
// Log the post ID being fetched in debug mode // Log the post ID being fetched in debug mode
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -44,12 +48,12 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
let highlighted_comment = &req.param("comment_id").unwrap_or_default(); let highlighted_comment = &req.param("comment_id").unwrap_or_default();
// Send a request to the url, receive JSON in response // Send a request to the url, receive JSON in response
match json(path).await { match json(path, quarantined).await {
// Otherwise, grab the JSON output from the request // Otherwise, grab the JSON output from the request
Ok(res) => { Ok(response) => {
// Parse the JSON into Post and Comment structs // Parse the JSON into Post and Comment structs
let post = parse_post(&res[0]).await; let post = parse_post(&response[0]).await;
let comments = parse_comments(&res[1], &post.permalink, &post.author.name, highlighted_comment).await; let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment);
// Use the Post and Comment structs to generate a website to show users // Use the Post and Comment structs to generate a website to show users
template(PostTemplate { template(PostTemplate {
@ -61,7 +65,14 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
}) })
} }
// If the Reddit API returns an error, exit and send error page to user // If the Reddit API returns an error, exit and send error page to user
Err(msg) => error(req, msg).await, Err(msg) => {
if msg == "quarantined" {
let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub)
} else {
error(req, msg).await
}
}
} }
} }
@ -106,6 +117,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
media, media,
thumbnail: Media { thumbnail: Media {
url: format_url(val(post, "thumbnail").as_str()), url: format_url(val(post, "thumbnail").as_str()),
alt_url: String::new(),
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(), width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(), height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
poster: "".to_string(), poster: "".to_string(),
@ -137,79 +149,71 @@ async fn parse_post(json: &serde_json::Value) -> Post {
} }
// COMMENTS // COMMENTS
#[async_recursion] fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
async fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> { // Parse the comment JSON into a Vector of Comments
// Separate the comment JSON into a Vector of comments let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
let comment_data = match json["data"]["children"].as_array() {
Some(f) => f.to_owned(),
None => Vec::new(),
};
let mut comments: Vec<Comment> = Vec::new();
// For each comment, retrieve the values to build a Comment object // For each comment, retrieve the values to build a Comment object
for comment in comment_data {
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
let data = &comment["data"];
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
let (rel_time, created) = time(unix_time);
let edited = match data["edited"].as_f64() {
Some(stamp) => time(stamp),
None => (String::new(), String::new()),
};
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
let replies: Vec<Comment> = if data["replies"].is_object() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment).await
} else {
Vec::new()
};
let parent_kind_and_id = val(&comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let id = val(&comment, "id");
let highlighted = id == highlighted_comment;
comments.push(Comment {
id,
kind,
parent_id: parent_info[1].to_string(),
parent_kind: parent_info[0].to_string(),
post_link: post_link.to_string(),
post_author: post_author.to_string(),
body,
author: Author {
name: val(&comment, "author"),
flair: Flair {
flair_parts: FlairPart::parse(
data["author_flair_type"].as_str().unwrap_or_default(),
data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(),
),
text: esc!(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"),
},
distinguished: val(&comment, "distinguished"),
},
score: if data["score_hidden"].as_bool().unwrap_or_default() {
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},
rel_time,
created,
edited,
replies,
highlighted,
});
}
comments comments
.into_iter()
.map(|comment| {
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
let data = &comment["data"];
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
let (rel_time, created) = time(unix_time);
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
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
let replies: Vec<Comment> = if data["replies"].is_object() {
parse_comments(&data["replies"], post_link, post_author, highlighted_comment)
} else {
Vec::new()
};
let parent_kind_and_id = val(&comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let id = val(&comment, "id");
let highlighted = id == highlighted_comment;
Comment {
id,
kind,
parent_id: parent_info[1].to_string(),
parent_kind: parent_info[0].to_string(),
post_link: post_link.to_string(),
post_author: post_author.to_string(),
body,
author: Author {
name: val(&comment, "author"),
flair: Flair {
flair_parts: FlairPart::parse(
data["author_flair_type"].as_str().unwrap_or_default(),
data["author_flair_richtext"].as_array(),
data["author_flair_text"].as_str(),
),
text: esc!(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"),
},
distinguished: val(&comment, "distinguished"),
},
score: if data["score_hidden"].as_bool().unwrap_or_default() {
("\u{2022}".to_string(), "Hidden".to_string())
} else {
format_num(score)
},
rel_time,
created,
edited,
replies,
highlighted,
}
})
.collect()
} }

View File

@ -1,6 +1,10 @@
// CRATES // CRATES
use crate::utils::{cookie, error, format_num, format_url, param, template, val, Post, Preferences}; use crate::utils::{catch_random, error, format_num, format_url, param, redirect, setting, template, val, Post, Preferences};
use crate::{client::json, RequestExt}; use crate::{
client::json,
subreddit::{can_access_quarantine, quarantine},
RequestExt,
};
use askama::Template; use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
@ -31,28 +35,34 @@ struct SearchTemplate {
sub: String, sub: String,
params: SearchParams, params: SearchParams,
prefs: Preferences, prefs: Preferences,
url: String,
} }
// 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 cookie(&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?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
let query = param(&path, "q").unwrap_or_default();
if query.is_empty() {
return Ok(redirect("/".to_string()));
}
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let query = param(&path, "q"); let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits
if let Ok(random) = catch_random(&sub, "/find").await {
return Ok(random);
}
let sort = if param(&path, "sort").is_empty() { let sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
"relevance".to_string()
} else {
param(&path, "sort")
};
let subreddits = if param(&path, "restrict_sr").is_empty() { // If search is not restricted to this subreddit, show other subreddits in search results
search_subreddits(&query).await let subreddits = param(&path, "restrict_sr").map_or(search_subreddits(&query).await, |_| Vec::new());
} else {
Vec::new()
};
match Post::fetch(&path, String::new()).await { let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
match Post::fetch(&path, String::new(), quarantined).await {
Ok((posts, after)) => template(SearchTemplate { Ok((posts, after)) => template(SearchTemplate {
posts, posts,
subreddits, subreddits,
@ -60,14 +70,22 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
params: SearchParams { params: SearchParams {
q: query.replace('"', "&quot;"), q: query.replace('"', "&quot;"),
sort, sort,
t: param(&path, "t"), t: param(&path, "t").unwrap_or_default(),
before: param(&path, "after"), before: param(&path, "after").unwrap_or_default(),
after, after,
restrict_sr: param(&path, "restrict_sr"), restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
}, },
prefs: Preferences::new(req), prefs: Preferences::new(req),
url,
}), }),
Err(msg) => error(req, msg).await, Err(msg) => {
if msg == "quarantined" {
let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub)
} else {
error(req, msg).await
}
}
} }
} }
@ -75,35 +93,25 @@ async fn search_subreddits(q: &str) -> Vec<Subreddit> {
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+")); let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
// Send a request to the url // Send a request to the url
match json(subreddit_search_path).await { json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
// If success, receive JSON in response .as_array()
Ok(response) => { .map(ToOwned::to_owned)
match response["data"]["children"].as_array() { .unwrap_or_default()
// For each subreddit from subreddit list .iter()
Some(list) => list .map(|subreddit| {
.iter() // For each subreddit from subreddit list
.map(|subreddit| { // Fetch subreddit icon either from the community_icon or icon_img value
// Fetch subreddit icon either from the community_icon or icon_img value let icon = subreddit["data"]["community_icon"]
let community_icon: &str = subreddit["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]); .as_str()
let icon = if community_icon.is_empty() { .map_or_else(|| val(subreddit, "icon_img"), ToString::to_string);
val(&subreddit, "icon_img")
} else {
community_icon.to_string()
};
Subreddit { Subreddit {
name: val(subreddit, "display_name_prefixed"), name: val(subreddit, "display_name_prefixed"),
url: val(subreddit, "url"), url: val(subreddit, "url"),
icon: format_url(&icon), icon: format_url(&icon),
description: val(subreddit, "public_description"), description: val(subreddit, "public_description"),
subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64), subscribers: format_num(subreddit["data"]["subscribers"].as_f64().unwrap_or_default() as i64),
}
})
.collect::<Vec<Subreddit>>(),
_ => Vec::new(),
} }
} })
// If the Reddit API returns an error, exit this function .collect::<Vec<Subreddit>>()
_ => Vec::new(),
}
} }

View File

@ -53,7 +53,7 @@ pub trait ResponseExt {
impl RequestExt for Request<Body> { impl RequestExt for Request<Body> {
fn params(&self) -> Params { fn params(&self) -> Params {
self.extensions().get::<Params>().unwrap_or(&Params::new()).to_owned() self.extensions().get::<Params>().unwrap_or(&Params::new()).clone()
// self.extensions() // self.extensions()
// .get::<RequestMeta>() // .get::<RequestMeta>()
// .and_then(|meta| meta.route_params()) // .and_then(|meta| meta.route_params())
@ -69,29 +69,31 @@ impl RequestExt for Request<Body> {
} }
fn cookies(&self) -> Vec<Cookie> { fn cookies(&self) -> Vec<Cookie> {
let mut cookies = Vec::new(); self.headers().get("Cookie").map_or(Vec::new(), |header| {
if let Some(header) = self.headers().get("Cookie") { header
for cookie in header.to_str().unwrap_or_default().split("; ") { .to_str()
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named(""))); .unwrap_or_default()
} .split("; ")
} .map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
cookies .collect()
})
} }
fn cookie(&self, name: &str) -> Option<Cookie> { fn cookie(&self, name: &str) -> Option<Cookie> {
self.cookies().iter().find(|c| c.name() == name).map(std::borrow::ToOwned::to_owned) self.cookies().into_iter().find(|c| c.name() == name)
} }
} }
impl ResponseExt for Response<Body> { impl ResponseExt for Response<Body> {
fn cookies(&self) -> Vec<Cookie> { fn cookies(&self) -> Vec<Cookie> {
let mut cookies = Vec::new(); self.headers().get("Cookie").map_or(Vec::new(), |header| {
for header in self.headers().get_all("Cookie") { header
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) { .to_str()
cookies.push(cookie); .unwrap_or_default()
} .split("; ")
} .map(|cookie| Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")))
cookies .collect()
})
} }
fn insert_cookie(&mut self, cookie: Cookie) { fn insert_cookie(&mut self, cookie: Cookie) {
@ -144,6 +146,7 @@ impl Server {
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> { pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
let make_svc = make_service_fn(move |_conn| { let make_svc = make_service_fn(move |_conn| {
// For correct borrowing, these values need to be borrowed
let router = self.router.clone(); let router = self.router.clone();
let default_headers = self.default_headers.clone(); let default_headers = self.default_headers.clone();
@ -159,7 +162,7 @@ impl Server {
let mut path = req.uri().path().replace("//", "/"); let mut path = req.uri().path().replace("//", "/");
// Remove trailing slashes // Remove trailing slashes
if path.ends_with('/') && path != "/" { if path != "/" && path.ends_with('/') {
path.pop(); path.pop();
} }
@ -168,7 +171,7 @@ impl Server {
// If a route was configured for this path // If a route was configured for this path
Ok(found) => { Ok(found) => {
let mut parammed = req; let mut parammed = req;
parammed.set_params(found.params().to_owned()); parammed.set_params(found.params().clone());
// Run the route's function // Run the route's function
let func = (found.handler().to_owned().to_owned())(parammed); let func = (found.handler().to_owned().to_owned())(parammed);
@ -198,17 +201,15 @@ impl Server {
} }
}); });
// Build SocketAddr from provided address
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr)); let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr));
let server = HyperServer::bind(address).serve(make_svc); // Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler");
});
let graceful = server.with_graceful_shutdown(shutdown_signal()); server.boxed()
graceful.boxed()
} }
} }
async fn shutdown_signal() {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler");
}

View File

@ -16,6 +16,21 @@ struct SettingsTemplate {
prefs: Preferences, prefs: Preferences,
} }
// CONSTANTS
const PREFS: [&str; 10] = [
"theme",
"front_page",
"layout",
"wide",
"comment_sort",
"post_sort",
"show_nsfw",
"use_hls",
"hide_hls_notification",
"autoplay_videos",
];
// FUNCTIONS // FUNCTIONS
// Retrieve cookies from request "Cookie" header // Retrieve cookies from request "Cookie" header
@ -29,12 +44,12 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let (parts, mut body) = req.into_parts(); let (parts, mut body) = req.into_parts();
// Grab existing cookies // Grab existing cookies
let mut cookies = Vec::new(); let _cookies: Vec<Cookie> = parts
for header in parts.headers.get_all("Cookie") { .headers
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) { .get_all("Cookie")
cookies.push(cookie); .iter()
} .filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
} .collect();
// Aggregate the body... // Aggregate the body...
// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?; // let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
@ -48,64 +63,72 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>(); let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
let mut res = redirect("/settings".to_string()); let mut response = redirect("/settings".to_string());
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw"]; for &name in &PREFS {
for name in names {
match form.get(name) { match form.get(name) {
Some(value) => res.insert_cookie( Some(value) => response.insert_cookie(
Cookie::build(name.to_owned(), value.to_owned()) Cookie::build(name.to_owned(), value.clone())
.path("/") .path("/")
.http_only(true) .http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52)) .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.finish(), .finish(),
), ),
None => res.remove_cookie(name.to_string()), None => response.remove_cookie(name.to_string()),
}; };
} }
Ok(res) Ok(response)
} }
// Set cookies using response "Set-Cookie" header fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body> {
pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
// Split the body into parts // Split the body into parts
let (parts, _) = req.into_parts(); let (parts, _) = req.into_parts();
// Grab existing cookies // Grab existing cookies
let mut cookies = Vec::new(); let _cookies: Vec<Cookie> = parts
for header in parts.headers.get_all("Cookie") { .headers
if let Ok(cookie) = Cookie::parse(header.to_str().unwrap_or_default()) { .get_all("Cookie")
cookies.push(cookie); .iter()
} .filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
} .collect();
let query = parts.uri.query().unwrap_or_default().as_bytes(); let query = parts.uri.query().unwrap_or_default().as_bytes();
let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>(); let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"];
let path = match form.get("redirect") { let path = match form.get("redirect") {
Some(value) => format!("/{}/", value), Some(value) => format!("/{}", value.replace("%26", "&").replace("%23", "#")),
None => "/".to_string(), None => "/".to_string(),
}; };
let mut res = redirect(path); let mut response = redirect(path);
for name in names { for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
match form.get(name) { match form.get(name) {
Some(value) => res.insert_cookie( Some(value) => response.insert_cookie(
Cookie::build(name.to_owned(), value.to_owned()) Cookie::build(name.to_owned(), value.clone())
.path("/") .path("/")
.http_only(true) .http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52)) .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.finish(), .finish(),
), ),
None => res.remove_cookie(name.to_string()), None => {
if remove_cookies {
response.remove_cookie(name.to_string());
}
}
}; };
} }
Ok(res) response
}
// Set cookies using response "Set-Cookie" header
pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
Ok(set_cookies_method(req, true))
}
pub async fn update(req: Request<Body>) -> Result<Response<Body>, String> {
Ok(set_cookies_method(req, false))
} }

View File

@ -1,6 +1,6 @@
// CRATES // CRATES
use crate::esc; use crate::esc;
use crate::utils::{cookie, error, format_num, format_url, param, redirect, rewrite_urls, template, val, Post, Preferences, Subreddit}; use crate::utils::{catch_random, error, format_num, format_url, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit};
use crate::{client::json, server::ResponseExt, RequestExt}; use crate::{client::json, server::ResponseExt, RequestExt};
use askama::Template; use askama::Template;
use cookie::Cookie; use cookie::Cookie;
@ -16,6 +16,7 @@ struct SubredditTemplate {
sort: (String, String), sort: (String, String),
ends: (String, String), ends: (String, String),
prefs: Preferences, prefs: Preferences,
url: String,
} }
#[derive(Template)] #[derive(Template)]
@ -27,11 +28,22 @@ struct WikiTemplate {
prefs: Preferences, prefs: Preferences,
} }
#[derive(Template)]
#[template(path = "wall.html", escape = "none")]
struct WallTemplate {
title: String,
sub: String,
msg: String,
prefs: Preferences,
url: String,
}
// SERVICES // SERVICES
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path // Build Reddit API path
let subscribed = cookie(&req, "subscriptions"); let root = req.uri().path() == "/";
let front_page = cookie(&req, "front_page"); let subscribed = setting(&req, "subscriptions");
let front_page = setting(&req, "front_page");
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort)); let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
@ -39,11 +51,17 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
if subscribed.is_empty() { if subscribed.is_empty() {
"popular".to_string() "popular".to_string()
} else { } else {
subscribed.to_owned() subscribed.clone()
} }
} else { } else {
front_page.to_owned() front_page.clone()
}); });
let quarantined = can_access_quarantine(&req, &sub) || root;
// Handle random subreddits
if let Ok(random) = catch_random(&sub, "").await {
return Ok(random);
}
if req.param("sub").is_some() && sub.starts_with("u_") { if req.param("sub").is_some() && sub.starts_with("u_") {
return Ok(redirect(["/user/", &sub[2..]].concat())); return Ok(redirect(["/user/", &sub[2..]].concat()));
@ -51,16 +69,16 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default()); let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
match Post::fetch(&path, String::new()).await { match Post::fetch(&path, String::new(), quarantined).await {
Ok((posts, after)) => { Ok((posts, after)) => {
// If you can get subreddit posts, also request subreddit metadata // If you can get subreddit posts, also request subreddit metadata
let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" { let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" {
// Regular subreddit // Regular subreddit
subreddit(&sub).await.unwrap_or_default() subreddit(&sub, quarantined).await.unwrap_or_default()
} else if sub == subscribed { } else if sub == subscribed {
// Subscription feed // Subscription feed
if req.uri().path().starts_with("/r/") { if req.uri().path().starts_with("/r/") {
subreddit(&sub).await.unwrap_or_default() subreddit(&sub, quarantined).await.unwrap_or_default()
} else { } else {
Subreddit::default() Subreddit::default()
} }
@ -74,16 +92,19 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
Subreddit::default() Subreddit::default()
}; };
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
template(SubredditTemplate { template(SubredditTemplate {
sub, sub,
posts, posts,
sort: (sort, param(&path, "t")), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after"), after), ends: (param(&path, "after").unwrap_or_default(), after),
prefs: Preferences::new(req), prefs: Preferences::new(req),
url,
}) })
} }
Err(msg) => match msg.as_str() { Err(msg) => match msg.as_str() {
"quarantined" => error(req, format!("r/{} has been quarantined by Reddit", sub)).await, "quarantined" => quarantine(req, sub),
"private" => error(req, format!("r/{} is a private community", sub)).await, "private" => error(req, format!("r/{} is a private community", sub)).await,
"banned" => error(req, format!("r/{} has been banned from Reddit", sub)).await, "banned" => error(req, format!("r/{} has been banned from Reddit", sub)).await,
_ => error(req, msg).await, _ => error(req, msg).await,
@ -91,44 +112,112 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
} }
} }
pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, String> {
let wall = WallTemplate {
title: format!("r/{} is quarantined", sub),
msg: "Please click the button below to continue to this subreddit.".to_string(),
url: req.uri().to_string(),
sub,
prefs: Preferences::new(req),
};
Ok(
Response::builder()
.status(403)
.header("content-type", "text/html")
.body(wall.render().unwrap_or_default().into())
.unwrap_or_default(),
)
}
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
let subreddit = req.param("sub").ok_or("Invalid URL")?;
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
let mut response = redirect(redir);
response.insert_cookie(
Cookie::build(&format!("allow_quaran_{}", subreddit.to_lowercase()), "true")
.path("/")
.http_only(true)
.expires(cookie::Expiration::Session)
.finish(),
);
Ok(response)
}
pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
// Determine if the subreddit can be accessed
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
}
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header // Sub or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
// Handle random subreddits
if sub == "random" || sub == "randnsfw" {
return Err("Can't subscribe to random subreddit!".to_string());
}
let query = req.uri().query().unwrap_or_default().to_string(); let query = req.uri().query().unwrap_or_default().to_string();
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect(); let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
let mut sub_list = Preferences::new(req).subscriptions; let mut sub_list = Preferences::new(req).subscriptions;
// Retrieve list of posts for these subreddits to extract display names
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await?;
let display_lookup: Vec<(String, &str)> = posts["data"]["children"]
.as_array()
.map(|list| {
list
.iter()
.map(|post| {
let display_name = post["data"]["subreddit"].as_str().unwrap_or_default();
(display_name.to_lowercase(), display_name)
})
.collect::<Vec<_>>()
})
.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('+') {
// Retrieve display name for the subreddit
let display;
let part = if let Some(&(_, display)) = display_lookup.iter().find(|x| x.0 == part.to_lowercase()) {
// This is already known, doesn't require seperate request
display
} else {
// This subreddit display name isn't known, retrieve it
let path: String = format!("/r/{}/about.json?raw_json=1", part);
display = json(path, true).await?;
display["data"]["display_name"].as_str().ok_or_else(|| "Failed to query subreddit name".to_string())?
};
// Modify sub list based on action // Modify sub list based on action
if action.contains(&"subscribe".to_string()) && !sub_list.contains(&part.to_owned()) { if action.contains(&"subscribe".to_string()) && !sub_list.contains(&part.to_owned()) {
// Add each sub name to the subscribed list // Add each sub name to the subscribed list
sub_list.push(part.to_owned()); sub_list.push(part.to_owned());
// Reorder sub names alphabettically // Reorder sub names alphabettically
sub_list.sort_by_key(|a| a.to_lowercase()) sub_list.sort_by_key(|a| a.to_lowercase());
} else if action.contains(&"unsubscribe".to_string()) { } else if action.contains(&"unsubscribe".to_string()) {
// Remove sub name from subscribed list // Remove sub name from subscribed list
sub_list.retain(|s| s != part); sub_list.retain(|s| s.to_lowercase() != part.to_lowercase());
} }
} }
// Redirect back to subreddit // Redirect back to subreddit
// check for redirect parameter if unsubscribing from outside sidebar // check for redirect parameter if unsubscribing from outside sidebar
let redirect_path = param(&format!("/?{}", query), "redirect"); let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
let path = if redirect_path.is_empty() {
format!("/r/{}", sub)
} else {
format!("/{}/", redirect_path) format!("/{}/", redirect_path)
} else {
format!("/r/{}", sub)
}; };
let mut res = redirect(path); let mut response = redirect(path);
// Delete cookie if empty, else set // Delete cookie if empty, else set
if sub_list.is_empty() { if sub_list.is_empty() {
res.remove_cookie("subscriptions".to_string()); response.remove_cookie("subscriptions".to_string());
} else { } else {
res.insert_cookie( response.insert_cookie(
Cookie::build("subscriptions", sub_list.join("+")) Cookie::build("subscriptions", sub_list.join("+"))
.path("/") .path("/")
.http_only(true) .http_only(true)
@ -137,75 +226,132 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
); );
} }
Ok(res) Ok(response)
} }
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string()); let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits
if let Ok(random) = catch_random(&sub, "/wiki").await {
return Ok(random);
}
let page = req.param("page").unwrap_or_else(|| "index".to_string()); let page = req.param("page").unwrap_or_else(|| "index".to_string());
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page); let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
match json(path).await { match json(path, quarantined).await {
Ok(response) => template(WikiTemplate { Ok(response) => template(WikiTemplate {
sub, sub,
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or_default()), wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
page, page,
prefs: Preferences::new(req), prefs: Preferences::new(req),
}), }),
Err(msg) => error(req, msg).await, Err(msg) => {
if msg == "quarantined" {
quarantine(req, sub)
} else {
error(req, msg).await
}
}
} }
} }
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string()); let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
let quarantined = can_access_quarantine(&req, &sub);
// Handle random subreddits
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
return Ok(random);
}
// Build the Reddit JSON API url // Build the Reddit JSON API url
let path: String = format!("/r/{}/about.json?raw_json=1", sub); let path: String = format!("/r/{}/about.json?raw_json=1", sub);
// Send a request to the url // Send a request to the url
match json(path).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 {
sub,
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")), wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")),
// wiki: format!(
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
// rewrite_urls(&val(&response, "description_html").replace("\\", "")),
// moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""),
// ),
sub,
page: "Sidebar".to_string(), page: "Sidebar".to_string(),
prefs: Preferences::new(req), prefs: Preferences::new(req),
}), }),
Err(msg) => error(req, msg).await, Err(msg) => {
if msg == "quarantined" {
quarantine(req, sub)
} else {
error(req, msg).await
}
}
} }
} }
// pub async fn moderators(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// // Retrieve and format the html for the moderators list
// Ok(
// moderators_list(sub, quarantined)
// .await?
// .iter()
// .map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
// .collect(),
// )
// }
// async fn moderators_list(sub: &str, quarantined: bool) -> Result<Vec<String>, String> {
// // Build the moderator list URL
// let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
// // Retrieve response
// json(path, quarantined).await.map(|response| {
// // Traverse json tree and format into list of strings
// response["data"]["children"]
// .as_array()
// .unwrap_or(&Vec::new())
// .iter()
// .filter_map(|moderator| {
// let name = moderator["name"].as_str().unwrap_or_default();
// if name.is_empty() {
// None
// } else {
// Some(name.to_string())
// }
// })
// .collect::<Vec<_>>()
// })
// }
// SUBREDDIT // SUBREDDIT
async fn subreddit(sub: &str) -> Result<Subreddit, String> { async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
// Build the Reddit JSON API url // Build the Reddit JSON API url
let path: String = format!("/r/{}/about.json?raw_json=1", sub); let path: String = format!("/r/{}/about.json?raw_json=1", sub);
// Send a request to the url // Send a request to the url
match json(path).await { let res = json(path, quarantined).await?;
// If success, receive JSON in response
Ok(res) => {
// Metadata regarding the subreddit
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
// Fetch subreddit icon either from the community_icon or icon_img value // Metadata regarding the subreddit
let community_icon: &str = res["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]); let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() }; let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
let sub = Subreddit { // Fetch subreddit icon either from the community_icon or icon_img value
name: esc!(&res, "display_name"), let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or_default();
title: esc!(&res, "title"), let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
description: esc!(&res, "public_description"),
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
icon: format_url(&icon),
members: format_num(members),
active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
};
Ok(sub) Ok(Subreddit {
} name: esc!(&res, "display_name"),
// If the Reddit API returns an error, exit this function title: esc!(&res, "title"),
Err(msg) => return Err(msg), description: esc!(&res, "public_description"),
} info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
icon: format_url(&icon),
members: format_num(members),
active: format_num(active),
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
})
} }

View File

@ -16,6 +16,7 @@ struct UserTemplate {
sort: (String, String), sort: (String, String),
ends: (String, String), ends: (String, String),
prefs: Preferences, prefs: Preferences,
url: String,
} }
// FUNCTIONS // FUNCTIONS
@ -28,11 +29,12 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
); );
// Retrieve other variables from Libreddit request // Retrieve other variables from Libreddit request
let sort = param(&path, "sort"); let sort = param(&path, "sort").unwrap_or_default();
let username = req.param("name").unwrap_or_default(); let username = req.param("name").unwrap_or_default();
// Request user posts/comments from Reddit // Request user posts/comments from Reddit
let posts = Post::fetch(&path, "Comment".to_string()).await; let posts = Post::fetch(&path, "Comment".to_string(), false).await;
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
match posts { match posts {
Ok((posts, after)) => { Ok((posts, after)) => {
@ -42,9 +44,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
template(UserTemplate { template(UserTemplate {
user, user,
posts, posts,
sort: (sort, param(&path, "t")), sort: (sort, param(&path, "t").unwrap_or_default()),
ends: (param(&path, "after"), after), ends: (param(&path, "after").unwrap_or_default(), after),
prefs: Preferences::new(req), prefs: Preferences::new(req),
url,
}) })
} }
// If there is an error show error page // If there is an error show error page
@ -58,27 +61,22 @@ async fn user(name: &str) -> Result<User, String> {
let path: String = format!("/user/{}/about.json?raw_json=1", name); let path: String = format!("/user/{}/about.json?raw_json=1", name);
// Send a request to the url // Send a request to the url
match json(path).await { json(path, false).await.map(|res| {
// If success, receive JSON in response // Grab creation date as unix timestamp
Ok(res) => { let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// 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();
// Parse the JSON output into a User struct // Parse the JSON output into a User struct
Ok(User { User {
name: name.to_string(), name: name.to_string(),
title: esc!(about("title")), title: esc!(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: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
banner: esc!(about("banner_img")), banner: esc!(about("banner_img")),
description: about("public_description"), description: about("public_description"),
})
} }
// If the Reddit API returns an error, exit this function })
Err(msg) => return Err(msg),
}
} }

View File

@ -39,7 +39,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" => value("t").to_string(), "text" => esc!(value("t")),
"emoji" => format_url(value("u")), "emoji" => format_url(value("u")),
_ => String::new(), _ => String::new(),
}, },
@ -52,7 +52,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: text.to_string(), value: esc!(text),
}], }],
None => Vec::new(), None => Vec::new(),
}, },
@ -75,6 +75,7 @@ pub struct Flags {
pub struct Media { pub struct Media {
pub url: String, pub url: String,
pub alt_url: String,
pub width: i64, pub width: i64,
pub height: i64, pub height: i64,
pub poster: String, pub poster: String,
@ -85,12 +86,28 @@ impl Media {
let mut gallery = Vec::new(); let mut gallery = Vec::new();
// If post is a video, return the video // If post is a video, return the video
let (post_type, url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() { let (post_type, url_val, alt_url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() {
// Return reddit video // Return reddit video
("video", &data["preview"]["reddit_video_preview"]["fallback_url"]) (
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
"gif"
} else {
"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 data["secure_media"]["reddit_video"]["fallback_url"].is_string() {
// Return reddit video // Return reddit video
("video", &data["secure_media"]["reddit_video"]["fallback_url"]) (
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
"gif"
} else {
"video"
},
&data["secure_media"]["reddit_video"]["fallback_url"],
Some(&data["secure_media"]["reddit_video"]["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
let preview = &data["preview"]["images"][0]; let preview = &data["preview"]["images"][0];
@ -98,26 +115,26 @@ impl Media {
if mp4.is_object() { if mp4.is_object() {
// Return the mp4 if the media is a gif // Return the mp4 if the media is a gif
("gif", &mp4["source"]["url"]) ("gif", &mp4["source"]["url"], None)
} else { } else {
// Return the picture if the media is an image // Return the picture if the media is an image
if data["domain"] == "i.redd.it" { if data["domain"] == "i.redd.it" {
("image", &data["url"]) ("image", &data["url"], None)
} else { } else {
("image", &preview["source"]["url"]) ("image", &preview["source"]["url"], None)
} }
} }
} else if data["is_self"].as_bool().unwrap_or_default() { } else if data["is_self"].as_bool().unwrap_or_default() {
// If type is self, return permalink // If type is self, return permalink
("self", &data["permalink"]) ("self", &data["permalink"], None)
} else if data["is_gallery"].as_bool().unwrap_or_default() { } else if data["is_gallery"].as_bool().unwrap_or_default() {
// If this post contains a gallery of images // If this post contains a gallery of images
gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]); gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]);
("gallery", &data["url"]) ("gallery", &data["url"], None)
} else { } else {
// If type can't be determined, return url // If type can't be determined, return url
("link", &data["url"]) ("link", &data["url"], None)
}; };
let source = &data["preview"]["images"][0]["source"]; let source = &data["preview"]["images"][0]["source"];
@ -128,10 +145,13 @@ impl Media {
format_url(url_val.as_str().unwrap_or_default()) 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()));
( (
post_type.to_string(), post_type.to_string(),
Self { Self {
url, 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(),
poster: format_url(source["url"].as_str().unwrap_or_default()), poster: format_url(source["url"].as_str().unwrap_or_default()),
@ -197,12 +217,12 @@ 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, fallback_title: String) -> Result<(Vec<Self>, String), String> { pub async fn fetch(path: &str, fallback_title: String, quarantine: bool) -> Result<(Vec<Self>, String), String> {
let res; let res;
let post_list; let post_list;
// Send a request to the url // Send a request to the url
match json(path.to_string()).await { match json(path.to_string(), quarantine).await {
// If success, receive JSON in response // If success, receive JSON in response
Ok(response) => { Ok(response) => {
res = response; res = response;
@ -229,11 +249,11 @@ impl Post {
let title = esc!(post, "title"); let title = esc!(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;
posts.push(Self { posts.push(Self {
id: val(post, "id"), id: val(post, "id"),
title: esc!(if title.is_empty() { fallback_title.to_owned() } else { title }), title: esc!(if title.is_empty() { fallback_title.clone() } else { title }),
community: val(post, "subreddit"), community: val(post, "subreddit"),
body: rewrite_urls(&val(post, "body_html")), body: rewrite_urls(&val(post, "body_html")),
author: Author { author: Author {
@ -259,6 +279,7 @@ impl Post {
post_type, post_type,
thumbnail: Media { thumbnail: Media {
url: format_url(val(post, "thumbnail").as_str()), url: format_url(val(post, "thumbnail").as_str()),
alt_url: String::new(),
width: data["thumbnail_width"].as_i64().unwrap_or_default(), width: data["thumbnail_width"].as_i64().unwrap_or_default(),
height: data["thumbnail_height"].as_i64().unwrap_or_default(), height: data["thumbnail_height"].as_i64().unwrap_or_default(),
poster: "".to_string(), poster: "".to_string(),
@ -341,6 +362,7 @@ pub struct Subreddit {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub info: String, pub info: String,
// pub moderators: Vec<String>,
pub icon: String, pub icon: String,
pub members: (String, String), pub members: (String, String),
pub active: (String, String), pub active: (String, String),
@ -364,6 +386,9 @@ pub struct Preferences {
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
pub show_nsfw: String, pub show_nsfw: String,
pub hide_hls_notification: String,
pub use_hls: String,
pub autoplay_videos: String,
pub comment_sort: String, pub comment_sort: String,
pub post_sort: String, pub post_sort: String,
pub subscriptions: Vec<String>, pub subscriptions: Vec<String>,
@ -373,14 +398,17 @@ impl Preferences {
// Build preferences from cookies // Build preferences from cookies
pub fn new(req: Request<Body>) -> Self { pub fn new(req: Request<Body>) -> Self {
Self { Self {
theme: cookie(&req, "theme"), theme: setting(&req, "theme"),
front_page: cookie(&req, "front_page"), front_page: setting(&req, "front_page"),
layout: cookie(&req, "layout"), layout: setting(&req, "layout"),
wide: cookie(&req, "wide"), wide: setting(&req, "wide"),
show_nsfw: cookie(&req, "show_nsfw"), show_nsfw: setting(&req, "show_nsfw"),
comment_sort: cookie(&req, "comment_sort"), use_hls: setting(&req, "use_hls"),
post_sort: cookie(&req, "post_sort"), hide_hls_notification: setting(&req, "hide_hls_notification"),
subscriptions: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), autoplay_videos: setting(&req, "autoplay_videos"),
comment_sort: setting(&req, "comment_sort"),
post_sort: setting(&req, "post_sort"),
subscriptions: setting(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
} }
} }
} }
@ -390,17 +418,46 @@ impl Preferences {
// //
// Grab a query parameter from a url // Grab a query parameter from a url
pub fn param(path: &str, value: &str) -> String { pub fn param(path: &str, value: &str) -> Option<String> {
match Url::parse(format!("https://libredd.it/{}", path).as_str()) { Some(
Ok(url) => url.query_pairs().into_owned().collect::<HashMap<_, _>>().get(value).unwrap_or(&String::new()).to_owned(), Url::parse(format!("https://libredd.it/{}", path).as_str())
_ => String::new(), .ok()?
} .query_pairs()
.into_owned()
.collect::<HashMap<_, _>>()
.get(value)?
.clone(),
)
} }
// Parse a cookie value from request // Retrieve the value of a setting by name
pub fn cookie(req: &Request<Body>, name: &str) -> String { pub fn setting(req: &Request<Body>, name: &str) -> String {
let cookie = req.cookie(name).unwrap_or_else(|| Cookie::named(name)); // Parse a cookie value from request
cookie.value().to_string() req
.cookie(name)
.unwrap_or_else(|| {
// If there is no cookie for this setting, try receiving a default from an environment variable
if let Ok(default) = std::env::var(format!("LIBREDDIT_DEFAULT_{}", name.to_uppercase())) {
Cookie::new(name, default)
} else {
Cookie::named(name)
}
})
.value()
.to_string()
}
// Detect and redirect in the event of a random subreddit
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
if (sub == "random" || sub == "randnsfw") && !sub.contains('+') {
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"]
.as_str()
.unwrap_or_default()
.to_string();
Ok(redirect(format!("/r/{}{}", new_sub, additional)))
} else {
Err("No redirect needed".to_string())
}
} }
// Direct urls to proxy if proxy is enabled // Direct urls to proxy if proxy is enabled
@ -408,47 +465,72 @@ 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 {
match Url::parse(url) { Url::parse(url).map_or(String::new(), |parsed| {
Ok(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| {
Regex::new(regex) Regex::new(regex).map_or(String::new(), |re| {
.map(|re| match re.captures(url) { re.captures(url).map_or(String::new(), |caps| match segments {
Some(caps) => match segments { 1 => [format, &caps[1]].join(""),
1 => [format, &caps[1]].join(""), 2 => [format, &caps[1], "/", &caps[2]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""), _ => String::new(),
_ => String::new(), })
}, })
None => String::new(), };
})
.unwrap_or_default() macro_rules! chain {
() => {
{
String::new()
}
}; };
match domain { ( $first_fn:expr, $($other_fns:expr), *) => {
"v.redd.it" => capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2), {
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1), let result = $first_fn;
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1), if result.is_empty() {
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1), chain!($($other_fns,)*)
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2), }
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1), else
"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), result
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1), }
_ => String::new(), }
} };
} }
Err(_) => String::new(),
} match domain {
"v.redd.it" => chain!(
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
),
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/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),
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
_ => String::new(),
}
})
} }
} }
// Rewrite Reddit links to Libreddit in body of text // Rewrite Reddit links to Libreddit in body of text
pub fn rewrite_urls(text: &str) -> String { pub fn rewrite_urls(input_text: &str) -> String {
match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) { let text1 =
Ok(re) => re.replace_all(text, r#"href="/"#).to_string(), 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());
Err(_) => String::new(),
} // Rewrite external media previews to Libreddit
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
if re.is_match(&text1) {
re.replace_all(&text1, format_url(re.find(&text1).map(|x| x.as_str()).unwrap_or_default())).to_string()
} else {
text1
}
})
} }
// Append `m` and `k` for millions and thousands respectively // Append `m` and `k` for millions and thousands respectively
@ -489,27 +571,17 @@ 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_export]
macro_rules! esc { macro_rules! esc {
($f:expr) => { ($f:expr) => {
$f.replace('<', "&lt;").replace('>', "&gt;") $f.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}; };
($j:expr, $k:expr) => { ($j:expr, $k:expr) => {
$j["data"][$k].as_str().unwrap_or_default().to_string().replace('<', "&lt;").replace('>', "&gt;") $j["data"][$k].as_str().unwrap_or_default().to_string().replace('<', "&lt;").replace('>', "&gt;")
}; };
} }
// Escape < and > to accurately render HTML
// pub fn esc(j: &Value, k: &str) -> String {
// val(j,k)
// // .replace('&', "&amp;")
// .replace('<', "&lt;")
// .replace('>', "&gt;")
// // .replace('"', "&quot;")
// // .replace('\'', "&#x27;")
// // .replace('/', "&#x2f;")
// }
// //
// NETWORKING // NETWORKING
// //

BIN
static/Inter.var.woff2 Normal file

Binary file not shown.

5
static/hls.min.js vendored Normal file

File diff suppressed because one or more lines are too long

77
static/playHLSVideo.js Normal file
View File

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

View File

@ -6,6 +6,12 @@
--admin: #ea0027; --admin: #ea0027;
} }
@font-face {
font-family: 'Inter';
src: url('/Inter.var.woff2') format('woff2-variations');
font-style: normal;
}
/* Automatic theme selection */ /* Automatic theme selection */
:root, .dark{ :root, .dark{
/* Default & fallback theme (dark) */ /* Default & fallback theme (dark) */
@ -18,6 +24,7 @@
--post: #161616; --post: #161616;
--panel-border: 1px solid #333; --panel-border: 1px solid #333;
--highlighted: #333; --highlighted: #333;
--visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5); --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
} }
@ -33,6 +40,7 @@
--post: #eee; --post: #eee;
--panel-border: 1px solid #ccc; --panel-border: 1px solid #ccc;
--highlighted: white; --highlighted: white;
--visited: #555;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
} }
@ -48,6 +56,7 @@
--post: #eee; --post: #eee;
--panel-border: 1px solid #ccc; --panel-border: 1px solid #ccc;
--highlighted: white; --highlighted: white;
--visited: #555;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
@ -62,6 +71,7 @@
--post: black; --post: black;
--panel-border: 2px solid #0f0f0f; --panel-border: 2px solid #0f0f0f;
--highlighted: #0f0f0f; --highlighted: #0f0f0f;
--visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
@ -72,10 +82,11 @@
--text: #f8f8f2; --text: #f8f8f2;
--foreground: #3d4051; --foreground: #3d4051;
--background: #282a36; --background: #282a36;
--outside: #44475a; --outside: #393c4d;
--post: #44475a; --post: #333544;
--panel-border: 2px solid #44475a; --panel-border: 2px solid #44475a;
--highlighted: #4e5267; --highlighted: #4e5267;
--visited: #969692;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
@ -90,8 +101,55 @@
--post: #434c5e; --post: #434c5e;
--panel-border: 2px solid #4c566a; --panel-border: 2px solid #4c566a;
--highlighted: #3b4252; --highlighted: #3b4252;
--visited: #a3a5aa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --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);
}
/* General */ /* General */
::selection { ::selection {
@ -107,7 +165,7 @@ 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 {
margin: 0; margin: 0;
color: var(--text); color: var(--text);
font-family: sans-serif; font-family: "Inter", sans-serif;
} }
body { body {
@ -621,6 +679,7 @@ a.search_subreddit:hover {
"post_score post_title post_thumbnail" 1fr "post_score post_title post_thumbnail" 1fr
"post_score post_media post_thumbnail" auto "post_score post_media post_thumbnail" auto
"post_score post_body post_thumbnail" auto "post_score post_body post_thumbnail" auto
"post_score post_notification post_thumbnail" auto
"post_score post_footer post_thumbnail" auto "post_score post_footer post_thumbnail" auto
/ minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px)); / minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px));
} }
@ -663,6 +722,21 @@ a.search_subreddit:hover {
grid-area: post_title; grid-area: post_title;
} }
.post:not(.highlighted) .post_title a:visited {
color: var(--visited);
}
.post_notification {
grid-area: post_notification;
margin: 5px 15px;
text-align: center;
font-size: 12px;
}
.post_notification a {
text-decoration: underline;
}
.post_flair { .post_flair {
background: var(--accent); background: var(--accent);
color: var(--background); color: var(--background);
@ -907,6 +981,7 @@ a.search_subreddit:hover {
font-weight: normal; font-weight: normal;
padding: 5px 5px; padding: 5px 5px;
margin: 5px 0; margin: 5px 0;
overflow: auto;
} }
.comment_body.highlighted { .comment_body.highlighted {
@ -952,6 +1027,10 @@ a.search_subreddit:hover {
background: var(--foreground); background: var(--foreground);
} }
summary.comment_data {
cursor: pointer;
}
.moderator, .admin { opacity: 1; } .moderator, .admin { opacity: 1; }
.op, .moderator, .admin { font-weight: bold; } .op, .moderator, .admin { font-weight: bold; }
@ -1134,7 +1213,7 @@ input[type="submit"] {
} }
.md code { .md code {
font-family: monospace; font-family: monospace, sans-serif;
font-size: 14px; font-size: 14px;
} }
@ -1213,6 +1292,7 @@ td, th {
"post_title post_title post_thumbnail" 1fr "post_title post_title post_thumbnail" 1fr
"post_media post_media post_thumbnail" auto "post_media post_media post_thumbnail" auto
"post_body post_body post_thumbnail" auto "post_body post_body post_thumbnail" auto
"post_notification post_notification post_thumbnail" auto
"post_score post_footer post_thumbnail" auto "post_score post_footer post_thumbnail" auto
/ auto 1fr fit-content(min(20%, 152px)); / auto 1fr fit-content(min(20%, 152px));
} }

View File

@ -63,28 +63,38 @@
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.media.url }}"/> <image width="100%" height="100%" href="{{ post.media.url }}"/>
<desc> <desc>
<img alt="Post image" src="{{ post.media.url }}"/> <img loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
</desc> </desc>
</svg> </svg>
</a> </a>
{% else if post.post_type == "video" || post.post_type == "gif" %} {% else if post.post_type == "video" || post.post_type == "gif" %}
<video class="post_media_video" src="{{ post.media.url }}" controls autoplay loop><a href={{ post.media.url }}>Video</a></video> {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<script src="/hls.min.js"></script>
<video class="post_media_video short hls_autoplay" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
<script src="/playHLSVideo.js"></script>
{% else %}
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
{% call utils::render_hls_notification(post.permalink[1..]) %}
{% endif %}
{% else if post.post_type == "gallery" %} {% else if post.post_type == "gallery" %}
<div class="gallery"> <div class="gallery">
{% for image in post.gallery -%} {% for image in post.gallery -%}
<figure> <figure>
<a href="{{ image.url }}" ><img alt="Gallery image" src="{{ image.url }}"/></a> <a href="{{ image.url }}" ><img loading="lazy" alt="Gallery image" src="{{ image.url }}"/></a>
<figcaption> <figcaption>
<p>{{ image.caption }}</p> <p>{{ image.caption }}</p>
{% if image.outbound_url.len() > 0 %} {% if image.outbound_url.len() > 0 %}
<p><a class="outbound_url" href="{{ image.outbound_url }}">{{ image.outbound_url }}</a> <p><a class="outbound_url" href="{{ image.outbound_url }}" rel="nofollow">{{ image.outbound_url }}</a>
{% endif %} {% endif %}
</figcaption> </figcaption>
</figure> </figure>
{%- endfor %} {%- endfor %}
</div> </div>
{% else if post.post_type == "link" %} {% else if post.post_type == "link" %}
<a id="post_url" href="{{ post.media.url }}">{{ post.media.url }}</a> <a id="post_url" href="{{ post.media.url }}" rel="nofollow">{{ post.media.url }}</a>
{% endif %} {% endif %}
<!-- POST BODY --> <!-- POST BODY -->
@ -93,7 +103,7 @@
<div class="post_footer"> <div class="post_footer">
<ul id="post_links"> <ul id="post_links">
<li><a href="/{{ post.id }}">permalink</a></li> <li><a href="/{{ post.id }}">permalink</a></li>
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li> <li><a href="https://reddit.com/{{ post.id }}" rel="nofollow">reddit</a></li>
</ul> </ul>
<p>{{ post.upvote_ratio }}% Upvoted</p> <p>{{ post.upvote_ratio }}% Upvoted</p>
</div> </div>

View File

@ -34,7 +34,7 @@
<div id="search_subreddits"> <div id="search_subreddits">
{% 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 src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div> <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_right"> <div class="search_subreddit_right">
<p class="search_subreddit_header"> <p class="search_subreddit_header">
<span class="search_subreddit_name">{{ subreddit.name }}</span> <span class="search_subreddit_name">{{ subreddit.name }}</span>
@ -68,6 +68,10 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
<footer> <footer>
{% if params.before != "" %} {% if params.before != "" %}

View File

@ -15,7 +15,7 @@
<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"], "system") %} {% call utils::options(prefs.theme, ["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"], "system") %}
</select> </select>
</div> </div>
<p>Interface</p> <p>Interface</p>
@ -33,6 +33,7 @@
</div> </div>
<div id="wide"> <div id="wide">
<label for="wide">Wide UI:</label> <label for="wide">Wide UI:</label>
<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> <p>Content</p>
@ -50,8 +51,24 @@
</div> </div>
<div id="show_nsfw"> <div id="show_nsfw">
<label for="show_nsfw">Show NSFW posts:</label> <label for="show_nsfw">Show NSFW posts:</label>
<input type="hidden" value="off" name="show_nsfw">
<input type="checkbox" name="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}> <input type="checkbox" name="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}>
</div> </div>
<div id="autoplay_videos">
<label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div>
<div id="use_hls">
<label for="use_hls">Use HLS for videos</label>
<input type="hidden" value="off" name="use_hls">
<input type="checkbox" name="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
</div>
<div id="hide_hls_notification">
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
<input type="hidden" value="off" name="hide_hls_notification">
<input type="checkbox" name="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
</div>
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>
</form> </form>
@ -68,10 +85,10 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<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 }}&subscriptions={{ prefs.subscriptions.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 }}&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") }}">this link</a>.</p>
</div> </div>
</div> </div>

View File

@ -52,6 +52,10 @@
{% call utils::post_in_list(post) %} {% call utils::post_in_list(post) %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
</div> </div>
<footer> <footer>
@ -74,7 +78,7 @@
</div> </div>
{% endif %} {% endif %}
<div id="sub_meta"> <div id="sub_meta">
<img 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> <p id="sub_title">{{ sub.title }}</p>
<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>
@ -99,7 +103,17 @@
</div> </div>
<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">{{ sub.info }}</div> <div id="sidebar_contents">
{{ sub.info }}
{# <hr>
<h2>Moderators</h2>
<br>
<ul>
{% for moderator in sub.moderators %}
<li><a style="color: var(--accent)" href="/u/{{ moderator }}">{{ moderator }}</a></li>
{% endfor %}
</ul> #}
</div>
</details> </details>
</aside> </aside>
{% endif %} {% endif %}

View File

@ -49,8 +49,11 @@
</details> </details>
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
</div> </div>
<footer> <footer>
@ -65,7 +68,7 @@
</div> </div>
<aside> <aside>
<div class="panel" id="user"> <div class="panel" id="user">
<img 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> <p id="user_title">{{ user.title }}</p>
<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>

View File

@ -55,8 +55,14 @@
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
{% macro render_hls_notification(redirect_url) -%}
{% if post.post_type == "video" && !post.media.alt_url.is_empty() && prefs.hide_hls_notification != "on" %}
<div class="post_notification"><p><a href="/settings/update/?use_hls=on&redirect={{ redirect_url }}">Enable HLS</a> to view with audio, or <a href="/settings/update/?hide_hls_notification=on&redirect={{ redirect_url }}">disable this notification</a></p></div>
{% endif %}
{%- endmacro %}
{% macro post_in_list(post) -%} {% macro post_in_list(post) -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}"> <div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
<p class="post_header"> <p class="post_header">
{% let community -%} {% let community -%}
{% if post.community.starts_with("u_") -%} {% if post.community.starts_with("u_") -%}
@ -74,7 +80,8 @@
{% 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 render_flair(post.flair.flair_parts) %}</a> style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
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> </p>
@ -87,16 +94,24 @@
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.media.url }}"/> <image width="100%" height="100%" href="{{ post.media.url }}"/>
<desc> <desc>
<img alt="Post image" src="{{ post.media.url }}"/> <img loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
</desc> </desc>
</svg> </svg>
</a> </a>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" controls loop autoplay><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls autoplay><a href={{ post.media.url }}>Video</a></video> {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<video class="post_media_video short" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" controls preload="none" {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" />
</video>
{% else %}
<video class="post_media_video short" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %}
{% else if post.post_type != "self" %} {% else if post.post_type != "self" %}
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}"> <a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
{% if post.thumbnail.url.is_empty() %} {% if post.thumbnail.url.is_empty() %}
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
<title>Thumbnail</title> <title>Thumbnail</title>
@ -106,7 +121,7 @@
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg"> <svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/> <image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
<desc> <desc>
<img alt="Thumbnail" src="{{ post.thumbnail.url }}"/> <img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
</desc> </desc>
</svg> </svg>
{% endif %} {% endif %}

13
templates/wall.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}{{ msg }}{% endblock %}
{% block sortstyle %}{% endblock %}
{% block content %}
<div id="wall">
<h1>{{ title }}</h1>
<br>
<p>{{ msg }}</p>
<form action="/r/{{ sub }}?redir={{ url }}" method="POST">
<input id="save" type="submit" value="Continue">
</form>
</div>
{% endblock %}