Compare commits

..

89 commits

Author SHA1 Message Date
Matthew Esposito
9d948abadc
Merge pull request #831 from bennettmsherman/header-filters
Remove Reddit's 'Nel' and 'Report-To' (network error logging) response headers
2023-11-29 09:25:16 -05:00
Spike
2815dc5209
Correct the shutdown announcement 2023-07-14 12:05:57 -07:00
Spike
00697c6ae4
Add shut down announcement 2023-07-14 11:57:25 -07:00
Ben Sherman
7a14975fb8 Remove 'Nel' and 'Report-To' response headers 2023-07-08 19:20:58 -07:00
Matthew Esposito
ea696687be
Merge pull request #821 from fawni/feat/hide-subreddit-panel 2023-06-09 19:01:57 -04:00
fawn
13394b4a5e
Add ability to hide subreddit panel (closes #801) 2023-06-07 13:51:27 +03:00
Matthew Esposito
ba89b76332
Merge pull request #814 from Tokarak/deps-update 2023-06-04 18:14:27 -04:00
Matthew Esposito
96e9e0ea9f
Update .replit to download from nightly build artifacts (#815) 2023-06-03 23:36:39 +00:00
Matthew Esposito
c1dd1a091e
Update release binary paths 2023-06-03 16:30:58 -04:00
Matthew Esposito
05ae39f743
Update RUSTFLAGS 2023-06-03 16:15:24 -04:00
Matthew Esposito
221260c282
Remove MUSL, build statically via flags 2023-06-03 16:12:48 -04:00
Tokarak
f3c835bee7 Proof-read README.md 2023-06-03 20:02:02 +01:00
Tokarak
f9fd54aa3c Specify newer dependencies + cargo update 2023-06-03 19:41:32 +01:00
Matthew Esposito
510d967777
Add MUSL target 2023-06-03 14:33:27 -04:00
Matthew Esposito
0bcebff6f2
Fix YAML formatting 2023-06-03 14:24:19 -04:00
Matthew Esposito
0c74305617
Add MUSL builds to GH Actions and fix Release event trigger (#810) 2023-06-03 18:19:20 +00:00
Nazar
97f0f69059
Rebase #811 (#812)
Co-authored-by: Matthew Esposito <matt@matthew.science>
2023-06-03 17:32:46 +00:00
Matthew Esposito
b5fc4bef28
Fix github-actions versioning 2023-05-31 19:50:38 -04:00
Mathew Davies
81a6e6458c
ci: cleanup github actions (#803) 2023-05-31 23:47:58 +00:00
Matthew Esposito
193a6effbf
Merge pull request #792 from beucismis/master 2023-05-31 18:42:39 -04:00
Matthew Esposito
09551fca29
Merge pull request #806 from gmnsii/comment-searchbar-color 2023-05-31 18:40:25 -04:00
gmnsii
38ee0d9428 make comment search bar color change based on theme 2023-05-31 19:41:13 +02:00
Matthew Esposito
ca7ad9f812
Merge pull request #796 from StuffNoOneCaresAbout/lazy-init-regex 2023-05-01 10:09:59 -04:00
Matthew Esposito
98e2833881
Merge pull request #790 from StuffNoOneCaresAbout/allow-disabling-indexing 2023-05-01 10:08:20 -04:00
Kavin
4d5c52b83b
Rename variables to more descriptive names. 2023-05-01 05:00:49 +01:00
Kavin
6c47ea921b
performance: compile regex only once 2023-05-01 04:22:10 +01:00
beucismis
6c0e5cfe93 Add cursor:pointer for button and select 2023-04-29 21:16:02 +03:00
Kavin
0c591149d5
Add option to disable all indexing. 2023-04-26 12:52:12 +01:00
Kavin
8b4b2dd268
Ignore idea files. 2023-04-26 12:52:00 +01:00
Matthew Esposito
ac58bb532a
Merge pull request #787 from libreddit/clippy_refactor 2023-04-19 13:08:44 -04:00
Matthew Esposito
af8fe176ea
Fix clippy warnings 2023-04-19 10:37:47 -04:00
Matthew Esposito
bfa9c084bb
Merge pull request #786 from libreddit/update_deps 2023-04-19 10:32:46 -04:00
Matthew Esposito
3c892d3cfd
Update Cargo.lock - h2 moderate 2023-04-19 10:27:50 -04:00
Matthew Esposito
4a1b448abb
Merge pull request #776 from iTzBoboCz/polls 2023-04-17 18:12:02 -04:00
Matthew Esposito
991677cd1e
Add variable for now_utc, format 2023-04-17 18:00:41 -04:00
Matthew Esposito
3b8a13d050
Merge pull request #773 from libreddit/fmt_clippy 2023-04-15 11:01:19 -04:00
Matthew Esposito
0e90ebc1a1
Merge pull request #769 from gmnsii/bypass-gate 2023-04-15 11:00:20 -04:00
Matthew Esposito
af89d4c88f
Merge pull request #778 from Akanksh12/comments-to-contrib-files 2023-04-15 10:59:28 -04:00
Matthew Esposito
5f87875b8e
Merge branch 'master' into bypass-gate 2023-04-15 10:56:28 -04:00
Matthew Esposito
aaf05de1a8
Merge pull request #771 from gmnsii/comment-search 2023-04-15 10:55:10 -04:00
Akanksh Chitimalla
17f7f6a9d1 changed default port to 12345 2023-04-08 21:17:19 +05:30
Ondřej Pešek
ec226e0cab fix(polls): apply clippy suggestions 2023-04-08 10:41:12 +02:00
Matthew Esposito
2b8931c032
Merge pull request #770 from invakid404/patch-1
fix(style): fit footer width to body size
2023-04-07 12:05:41 -04:00
Matthew Esposito
62771bf4a3
Merge pull request #751 from master-hax/optimize-docker
optimize arm dockerfile
2023-04-07 12:02:03 -04:00
Akanksh Chitimalla
22e3e0eb91 added comments to libreddit.service and .conf 2023-04-06 10:06:37 +05:30
Ondřej Pešek
94a781c82c fix(polls): minor improvements 2023-04-01 14:31:39 +02:00
Ondřej Pešek
75af984154 fix(polls): apply suggestions and fix id parsing 2023-04-01 14:26:04 +02:00
Ondřej Pešek
8bed342a6d fix: print time suffix only for relative dates 2023-04-01 13:21:15 +02:00
gmnsii
de5d8d5f86 Requested code style changes 2023-03-26 11:52:02 -07:00
Matthew Esposito
f465394f93
Address fmt + clippy 2023-03-25 16:32:42 -04:00
gmnsii
1e418619f1 Feat: search for comments within posts
Add the ability to search for specific comments within posts.
Known issues:
  - Just like on reddit, this does not work with comment sorting. The
    sorting order is ignored during the search and changing the sorting
    order after the search does not change anything. I do not think we
    can fix this before reddit does, since in my understanding we rely
    on them for the sorting. However we could implement a default
    sorting method ourselves by taking the vector of comments returned
    from the search and sorting it manually.
  - The UI could be improved on mobile. On screens with a max width
    inferior to 480 pixels, the comment search bar is displayed below
    the comment sorting form. It would be great if we could make the
    search bar have the same width as the whole comment sorting form
    but I do not have the willpower to write any more css.
2023-03-24 17:41:26 -07:00
gmnsii
8be69f6fe5 Checks if the link contains the parameter instead of ends with it
To know if the gate should be bypassed, we check if the link contains
the pasameter instead of checking if the link ends with it. This is
impostant, for example if we were to implement searching for comments
within a post. If we wanted to search for comments within a post that we
have bypassed the gate to view: the link will look like
https://libreddit-instance/r/somesub/comments/post-id/post-title&bypass_nsfw_landing/?q=some-query&type=comment
2023-03-23 12:36:04 -07:00
gmnsii
a0726c5903 Change the bypass message and format code
The bypass message now indicates that the bypass is only temporary.
2023-03-23 11:09:33 -07:00
Ondřej Pešek
c1c867a5ff feat: add polls 2023-03-23 13:21:09 +01:00
Ondřej Pešek
5dc3279ac3 fix: make time work with future dates 2023-03-23 13:18:48 +01:00
Tsvetomir Bonev
dead990ba0
fix(style): fit footer width to body size 2023-03-23 13:49:40 +02:00
gmnsii
e046144bf3 Allow bypassing nsfw gate for posts
On instances that are not sfw-only, the nsfw gate for posts can now be
bypassed.
2023-03-22 23:18:35 -07:00
kuanhulio
e25622dac2
harden docker-compose.yml (#760)
`user: nobody`: the least privileged account.
`read_only: true`: this container doesn't write anything to the filesystem, this removes a vector.
`security_opt`: disallows the container to grab more privileges.
`cap_drop`: this container doesn't need any capabilities, drop them.
`networks`: put `libreddit` into its own network so it cannot see other containers by default.
2023-03-17 10:17:01 -06:00
Daniel Valentine
6bcc4aa368
Update version string in Cargo.lock. 2023-03-17 09:36:52 -06:00
Vivek
6d652fc38c
optimize arm dockerfile 2023-03-12 23:36:25 -07:00
Daniel Valentine
f62f7bf200
v0.30.1 2023-03-10 21:34:42 -07:00
Daniel Valentine
aece392a86
Pad bottom of body to prevent footer collision (fixes #747) 2023-03-10 21:33:45 -07:00
xatier
aeeb066e47
Update README.md (#748)
* Remove duplicated config

Was accidentally introduced in  412ce8f1f3
2023-03-10 21:04:05 -07:00
Daniel Valentine
51cdf574f7
v0.30.0 2023-03-08 22:15:31 -07:00
Spike
af6722c053
Move unimportant links to footer (#728) 2023-03-08 22:14:43 -07:00
Matthew Esposito
412ce8f1f3
Fix default subscriptions (#732)
Co-authored-by: Daniel Valentine <daniel@vielle.ws>
2023-03-08 21:53:23 -07:00
o69mar
dfa57c890d
fix build error on windows (#741) 2023-03-08 21:32:41 -07:00
mikupls
01f9907aaf
show the count of 'more replies'. (#740)
Co-authored-by: Daniel Valentine <daniel@vielle.ws>
Co-authored-by: Matthew Esposito <matt@matthew.science>
2023-03-08 21:30:41 -07:00
mikupls
bf19ff513f
add support for gifs in galleries. (#744) 2023-03-08 21:04:26 -07:00
mikupls
ffc9ca2e98
use the documented LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION config option. (#737) 2023-03-04 13:04:40 -07:00
Daniel Valentine
cef9266648
Restructure section on Libreddit user privacy. 2023-02-26 03:35:36 -07:00
Daniel Valentine
d3b4f4e379
Update tempfile to v3.4.0. 2023-02-26 03:11:17 -07:00
Daniel Valentine
b90b41c009
v0.29.4 2023-02-26 03:01:35 -07:00
pin
0eccb9bcf2
Add NetBSD install (#720) 2023-02-26 01:13:56 -07:00
domve
eb07a2ce7c
Make gated subreddits accessible by treating them as quarantined (#722)
* Fix gated communities being unviewable by treating them as quarantined

* Show restriction reason in quarantine template

* Add `gated` checks for other requests
2023-02-26 00:40:32 -07:00
wsy2220
0b39d4f059
Mark search query as safe on Prev/Next button (#731)
Fixes: #677 again. Complement to #686.
2023-02-26 00:35:05 -07:00
wsy2220
58fa213be8
Reuse hyper client. (#727)
Making a new connection on every request is very slow and wasteful, espectially on slower network.

Fix this by reuse a hyper client which shares a connection pool.

I'm able to lower /r/popular loading time from 5s to 1.5s on my machine.
2023-02-26 00:33:55 -07:00
Spike
5e03d701e4
Revert "Move unimportant links to footer"
This reverts commit e3df3a9470.
2023-02-19 18:03:55 +00:00
Spike
e3df3a9470
Move unimportant links to footer 2023-02-19 18:00:56 +00:00
Daniel Valentine
35504eda14
v0.29.3 -- fix layout bugs on mobile
Addresses the following layout bugs in mobile view:

* improper rendering of award images on posts
* upvote ratio no longer appearing on bottom-right corner of post as
  before
* Reddit warning pop-up background cut off at bottom of page

Fixes #713.
2023-02-14 20:19:19 -07:00
Daniel Valentine
a05cfe60fe
v0.29.2 2023-02-12 03:36:48 -07:00
Daniel Valentine
2774d15298
Fix bug causing user/sub title to appear off-center. 2023-02-12 01:02:25 -07:00
Spike
f544daf8c0
Replace snoo with r/ icon 2023-02-09 21:40:51 -08:00
Daniel Valentine
089315f9bb
v0.29.1 (fixes #713) 2023-02-09 22:25:42 -07:00
Daniel Valentine
1f7e14dd4e
v0.29.0 2023-02-08 00:33:57 -07:00
Daniel Valentine
37f71c48d1
Reduce size of instance info button in footer. 2023-02-08 00:33:31 -07:00
potatoesAreGod
fa68bf561b
added leaving reddit dialog (#643) 2023-02-08 00:24:06 -07:00
spikecodes
a4eecb251e
Fix listing_options hidden overflow 2023-02-04 00:02:32 -08:00
potatoesAreGod
eb9ef9f6d9
added leaving reddit dialog (#643) 2023-01-12 01:46:56 -07:00
40 changed files with 1567 additions and 759 deletions

View file

@ -1,38 +0,0 @@
name: Docker ARM 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.arm
platforms: linux/arm64
push: true
tags: libreddit/libreddit:arm
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,41 +0,0 @@
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: libreddit/libreddit:armv7
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,44 +1,58 @@
name: Docker amd64 Build
name: Docker Build
on:
push:
paths-ignore:
- "**.md"
branches:
- master
- 'main'
- 'master'
jobs:
build-docker:
runs-on: ubuntu-latest
strategy:
matrix:
config:
- { platform: 'linux/amd64', tag: 'latest', dockerfile: 'Dockerfile' }
- { platform: 'linux/arm64', tag: 'latest-arm', dockerfile: 'Dockerfile.arm' }
- { platform: 'linux/arm/v7', tag: 'latest-armv7', dockerfile: 'Dockerfile.armv7' }
steps:
- uses: actions/checkout@v2
- name: Checkout sources
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v3
if: matrix.config.platform == 'linux/amd64'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: libreddit/libreddit
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
file: ./${{ matrix.config.dockerfile }}
platforms: ${{ matrix.config.platform }}
push: true
tags: libreddit/libreddit:latest
tags: libreddit/libreddit:${{ matrix.config.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max

78
.github/workflows/main-rust.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: Rust Build & Publish
on:
push:
paths-ignore:
- "**.md"
branches:
- 'main'
- 'master'
release:
types: [published]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Cache Packages
uses: Swatinem/rust-cache@v2
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
# Building actions
- name: Build
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu
- name: Calculate SHA512 checksum
run: sha512sum target/x86_64-unknown-linux-gnu/release/libreddit > libreddit.sha512
- name: Calculate SHA256 checksum
run: sha256sum target/x86_64-unknown-linux-gnu/release/libreddit > libreddit.sha256
- uses: actions/upload-artifact@v3
name: Upload a Build Artifact
with:
name: libreddit
path: |
target/x86_64-unknown-linux-gnu/release/libreddit
libreddit.sha512
libreddit.sha256
- name: Versions
id: version
run: echo "VERSION=$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')" >> "$GITHUB_OUTPUT"
# Publishing actions
- name: Publish to crates.io
if: github.event_name == 'release'
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Release
uses: softprops/action-gh-release@v1
if: github.base_ref != 'master' && github.event_name == 'release'
with:
tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
draft: true
files: |
target/x86_64-unknown-linux-gnu/release/libreddit
libreddit.sha512
libreddit.sha256
body: |
- ${{ github.event.head_commit.message }} ${{ github.sha }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

62
.github/workflows/pull-request.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Pull Request
on:
push:
branches:
- 'main'
- 'master'
pull_request:
branches:
- 'main'
- 'master'
jobs:
test:
name: cargo test
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Run cargo test
run: cargo test
format:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain with rustfmt component
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --all -- --check
clippy:
name: cargo clippy -- -D warnings
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain with clippy component
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: clippy
- name: Run cargo clippy
run: cargo clippy -- -D warnings

View file

@ -1,22 +0,0 @@
name: Tests
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View file

@ -1,59 +0,0 @@
name: Rust
on:
push:
paths-ignore:
- "**.md"
branches:
- master
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Cache Packages
uses: Swatinem/rust-cache@v1.0.1
- name: Build
run: cargo build --release
- name: Publish to crates.io
continue-on-error: true
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- uses: actions/upload-artifact@v2.2.1
name: Upload a Build Artifact
with:
name: libreddit
path: target/release/libreddit
- name: Versions
id: version
run: |
echo "::set-output name=version::$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')"
echo "::set-output name=tag::$(git describe --tags)"
- name: Calculate SHA512 checksum
run: sha512sum target/release/libreddit > libreddit.sha512
- name: Release
uses: softprops/action-gh-release@v1
if: github.base_ref != 'master'
with:
tag_name: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }} - ${{ github.event.head_commit.message }}
draft: true
files: |
target/release/libreddit
libreddit.sha512
body: |
- ${{ github.event.head_commit.message }} ${{ github.sha }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

5
.gitignore vendored
View file

@ -1 +1,4 @@
/target
/target
# Idea Files
.idea/

View file

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

View file

@ -21,6 +21,7 @@ Daniel Valentine <daniel@vielle.ws>
dbrennand <52419383+dbrennand@users.noreply.github.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com>
domve <domve@posteo.net>
Dyras <jevwmguf@duck.com>
Edward <101938856+EdwardLangdon@users.noreply.github.com>
elliot <75391956+ellieeet123@users.noreply.github.com>
@ -58,9 +59,12 @@ Nicholas Christopher <nchristopher@tuta.io>
Nick Lowery <ClockVapor@users.noreply.github.com>
Nico <github@dr460nf1r3.org>
NKIPSC <15067635+NKIPSC@users.noreply.github.com>
o69mar <119129086+o69mar@users.noreply.github.com>
obeho <71698631+obeho@users.noreply.github.com>
obscurity <z@x4.pm>
Om G <34579088+OxyMagnesium@users.noreply.github.com>
pin <90570748+0323pin@users.noreply.github.com>
potatoesAreGod <118043038+potatoesAreGod@users.noreply.github.com>
RiversideRocks <59586759+RiversideRocks@users.noreply.github.com>
robin <8597693+robrobinbin@users.noreply.github.com>
Robin <8597693+robrobinbin@users.noreply.github.com>
@ -87,5 +91,6 @@ Tsvetomir Bonev <invakid404@riseup.net>
Vladislav Nepogodin <nepogodin.vlad@gmail.com>
Walkx <walkxnl@gmail.com>
Wichai <1482605+Chengings@users.noreply.github.com>
wsy2220 <wsy@dogben.com>
xatier <xatierlike@gmail.com>
Zach <72994911+zachjmurphy@users.noreply.github.com>

787
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,20 +3,20 @@ name = "libreddit"
description = " Alternative private front-end to Reddit"
license = "AGPL-3.0"
repository = "https://github.com/spikecodes/libreddit"
version = "0.28.1"
version = "0.30.1"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2021"
[dependencies]
askama = { version = "0.11.1", default-features = false }
cached = "0.42.0"
askama = { version = "0.12.0", default-features = false }
cached = "0.43.0"
clap = { version = "4.1.1", default-features = false, features = ["std", "env"] }
regex = "1.7.1"
serde = { version = "1.0.152", features = ["derive"] }
cookie = "0.16.2"
cookie = "0.17.0"
futures-lite = "1.12.0"
hyper = { version = "0.14.23", features = ["full"] }
hyper-rustls = "0.23.2"
hyper-rustls = "0.24.0"
percent-encoding = "2.2.0"
route-recognizer = "0.3.1"
serde_json = "1.0.91"
@ -26,13 +26,13 @@ url = "2.3.1"
rust-embed = { version = "6.4.2", features = ["include-exclude"] }
libflate = "1.2.0"
brotli = { version = "3.3.4", features = ["std"] }
toml = "0.5.10"
toml = "0.7.4"
once_cell = "1.17.0"
serde_yaml = "0.9.16"
build_html = "2.2.0"
[dev-dependencies]
lipsum = "0.8.2"
lipsum = "0.9.0"
sealed_test = "1.0.0"
[profile.release]

View file

@ -7,6 +7,10 @@ RUN apk add --no-cache g++ git
WORKDIR /usr/src/libreddit
# cache dependencies in their own layer
COPY Cargo.lock Cargo.toml .
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo install --config net.git-fetch-with-cli=true --path . && rm -rf ./src
COPY . .
# net.git-fetch-with-cli is specified in order to prevent a potential OOM kill

View file

@ -2,11 +2,15 @@
> An alternative private front-end to Reddit
# ⚠️ Why do I get TOO MANY REQUESTS errors? ⚠️
## As of July 12th, 2023, Libreddit is currently not operational as Reddit's API changes, that were designed to kill third-party apps and content scrapers who don't pay [large fees](https://www.theverge.com/2023/5/31/23743993/reddit-apollo-client-api-cost), went into effect. [Read the full announcement here.](https://github.com/libreddit/libreddit/issues/840)
## One of the project maintainers is working towards keeping this project alive to some extent: https://github.com/libreddit/libreddit/issues/836
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png)
---
**10 second pitch:** Libreddit is a portmanteau of "libre" (meaning freedom) and "Reddit". It is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit).
**10-second pitch:** Libreddit is a portmanteau of "libre" (meaning freedom) and "Reddit". It is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit).
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
@ -31,7 +35,7 @@ I appreciate any donations! Your support allows me to continue developing Libred
🔗 **Want to automatically redirect Reddit links to Libreddit? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
[Follow this link](https://github.com/libreddit/libreddit-instances/blob/master/instances.md) for an up-to-date table of instances in markdown format. This list is also available as [a machine-readable JSON](https://github.com/libreddit/libreddit-instances/blob/master/instances.json).
[Follow this link](https://github.com/libreddit/libreddit-instances/blob/master/instances.md) for an up-to-date table of instances in Markdown format. This list is also available as [a machine-readable JSON](https://github.com/libreddit/libreddit-instances/blob/master/instances.json).
Both files are part of the [libreddit-instances](https://github.com/libreddit/libreddit-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [libreddit-instances README](https://github.com/libreddit/libreddit-instances/blob/master/README.md).
@ -55,7 +59,7 @@ Libreddit currently implements most of Reddit's (signed-out) functionalities but
## How does it compare to Teddit?
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Libreddit into an even more polished product.
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Libreddit into an even more polished product.
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.
@ -114,13 +118,21 @@ Results from Google PageSpeed Insights ([Libreddit Report](https://pagespeed.web
For transparency, I hope to describe all the ways Libreddit handles user privacy.
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs fetched to aid with troubleshooting.
#### Server
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
* **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs fetched to aid with troubleshooting.
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data.
* **Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data.
**Hosting:** The official instances are hosted on [Replit](https://replit.com/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
#### Official instance (libreddit.spike.codes)
The official instance is hosted at https://libreddit.spike.codes.
* **Server:** The official instance runs a production binary, and thus logs nothing.
* **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic.
* **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
---
@ -159,15 +171,29 @@ For ArchLinux users, Libreddit is available from the AUR as [`libreddit-git`](ht
```
yay -S libreddit-git
```
## 4) NetBSD/pkgsrc
## 4) GitHub Releases
For NetBSD users, Libreddit is available from the official repositories.
```
pkgin install libreddit
```
Or, if you prefer to build from source
```
cd /usr/pkgsrc/libreddit
make install
```
## 5) GitHub Releases
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/libreddit/libreddit/releases/latest).
## 5) Replit/Heroku/Glitch
## 6) Replit/Heroku/Glitch
> **Warning**
> These are free hosting options but they are *not* private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
> These are free hosting options, but they are *not* private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
<a href="https://repl.it/github/libreddit/libreddit"><img src="https://repl.it/badge/github/libreddit/libreddit" alt="Run on Repl.it" height="32" /></a>
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/libreddit/libreddit)
@ -187,29 +213,33 @@ libreddit
Assign a default value for each instance-specific setting by passing environment variables to Libreddit in the format `LIBREDDIT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|Name|Possible values|Default value|Description|
|-|-|-|-|
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
| Name | Possible values | Default value | Description |
|---------------------------|-----------------|------------------|-----------------------------------------------------------------------------------------------------------|
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `www.unddit.com` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
## Default User Settings
Assign a default value for each user-modifiable setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
| Name | Possible values | Default value |
|-------------------------|-----------------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
| `HIDE_AWARDS` | `["on", "off"]` | `off`
| Name | Possible values | Default value |
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
| `WIDE` | `["on", "off"]` | `off` |
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
| `SHOW_NSFW` | `["on", "off"]` | `off` |
| `BLUR_NSFW` | `["on", "off"]` | `off` |
| `USE_HLS` | `["on", "off"]` | `off` |
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
You can also configure Libreddit with a configuration file. An example `libreddit.toml` can be found below:

View file

@ -46,9 +46,21 @@
},
"LIBREDDIT_DEFAULT_HIDE_AWARDS": {
"required": false
}
},
"LIBREDDIT_BANNER": {
"required": false
},
"LIBREDDIT_ROBOTS_DISABLE_INDEXING": {
"required": false
},
"LIBREDDIT_DEFAULT_SUBSCRIPTIONS": {
"required": false
},
"LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
"required": false
},
"LIBREDDIT_PUSHSHIFT_FRONTEND": {
"required": false
}
}
}

View file

@ -1,7 +1,11 @@
use std::{
os::unix::process::ExitStatusExt,
process::{Command, ExitStatus, Output},
};
use std::process::{Command, ExitStatus, Output};
#[cfg(not(target_os = "windows"))]
use std::os::unix::process::ExitStatusExt;
#[cfg(target_os = "windows")]
use std::os::windows::process::ExitStatusExt;
fn main() {
let output = String::from_utf8(
Command::new("git")

View file

@ -1,2 +1,16 @@
ADDRESS=0.0.0.0
PORT=12345
#LIBREDDIT_DEFAULT_THEME=default
#LIBREDDIT_DEFAULT_FRONT_PAGE=default
#LIBREDDIT_DEFAULT_LAYOUT=card
#LIBREDDIT_DEFAULT_WIDE=off
#LIBREDDIT_DEFAULT_POST_SORT=hot
#LIBREDDIT_DEFAULT_COMMENT_SORT=confidence
#LIBREDDIT_DEFAULT_SHOW_NSFW=off
#LIBREDDIT_DEFAULT_BLUR_NSFW=off
#LIBREDDIT_DEFAULT_USE_HLS=off
#LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION=off
#LIBREDDIT_DEFAULT_AUTOPLAY_VIDEOS=off
#LIBREDDIT_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)
#LIBREDDIT_DEFAULT_HIDE_AWARDS=off
#LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off

View file

@ -5,8 +5,8 @@ After=network.service
[Service]
DynamicUser=yes
# Default Values
Environment=ADDRESS=0.0.0.0
Environment=PORT=8080
#Environment=ADDRESS=0.0.0.0
#Environment=PORT=8080
# Optional Override
EnvironmentFile=-/etc/libreddit.conf
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}

View file

@ -7,7 +7,18 @@ services:
container_name: "libreddit"
ports:
- 8080:8080
user: nobody
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks:
- libreddit
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
interval: 5m
timeout: 3s
networks:
libreddit:

View file

@ -1,7 +1,10 @@
use cached::proc_macro::cached;
use futures_lite::{future::Boxed, FutureExt};
use hyper::{body, body::Buf, client, header, Body, Method, Request, Response, Uri};
use hyper::client::HttpConnector;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector;
use libflate::gzip;
use once_cell::sync::Lazy;
use percent_encoding::{percent_encode, CONTROLS};
use serde_json::Value;
use std::{io, result::Result};
@ -11,6 +14,11 @@ use crate::server::RequestExt;
const REDDIT_URL_BASE: &str = "https://www.reddit.com";
static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
client::Client::builder().build(https)
});
/// Gets the canonical path for a resource on Reddit. This is accomplished by
/// making a `HEAD` request to Reddit at the path given in `path`.
///
@ -66,11 +74,8 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
// First parameter is target URL (mandatory).
let uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Prepare the HTTPS connector.
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
// 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.clone();
let mut builder = Request::get(uri);
@ -99,6 +104,8 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
rm("x-cdn-server-region");
rm("x-reddit-cdn");
rm("x-reddit-video-features");
rm("Nel");
rm("Report-To");
res
})
@ -123,11 +130,8 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// Build Reddit URL from path.
let url = format!("{}{}", REDDIT_URL_BASE, path);
// Prepare the HTTPS connector.
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_or_http().enable_http1().build();
// 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.clone();
// Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.)
@ -140,7 +144,14 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive")
.header("Cookie", if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%7D" } else { "" })
.header(
"Cookie",
if quarantine {
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D"
} else {
""
},
)
.body(Body::empty());
async move {

View file

@ -9,12 +9,16 @@ use std::{env::var, fs::read_to_string};
// first request) and contains the instance settings.
pub(crate) static CONFIG: Lazy<Config> = Lazy::new(Config::load);
// This serves as the frontend for the Pushshift API - on removed comments, this URL will
// be the base of a link, to display removed content (on another site).
pub(crate) const DEFAULT_PUSHSHIFT_FRONTEND: &str = "www.unddit.com";
/// Stores the configuration parsed from the environment variables and the
/// config file. `Config::Default()` contains None for each setting.
/// When adding more config settings, add it to `Config::load`,
/// `get_setting_from_config`, both below, as well as
/// instance_info::InstanceInfo.to_string(), README.md and app.json.
#[derive(Default, Serialize, Deserialize, Clone)]
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct Config {
#[serde(rename = "LIBREDDIT_SFW_ONLY")]
pub(crate) sfw_only: Option<String>,
@ -52,8 +56,20 @@ pub struct Config {
#[serde(rename = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
pub(crate) default_hide_awards: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
pub(crate) default_subscriptions: Option<String>,
#[serde(rename = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
#[serde(rename = "LIBREDDIT_BANNER")]
pub(crate) banner: Option<String>,
#[serde(rename = "LIBREDDIT_ROBOTS_DISABLE_INDEXING")]
pub(crate) robots_disable_indexing: Option<String>,
#[serde(rename = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>,
}
impl Config {
@ -68,6 +84,7 @@ impl Config {
// environment variables with "LIBREDDIT", then check the config, then if
// both are `None`, return a `None` via the `map_or_else` function
let parse = |key: &str| -> Option<String> { var(key).ok().map_or_else(|| get_setting_from_config(key, &config), Some) };
Self {
sfw_only: parse("LIBREDDIT_SFW_ONLY"),
default_theme: parse("LIBREDDIT_DEFAULT_THEME"),
@ -81,7 +98,11 @@ impl Config {
default_use_hls: parse("LIBREDDIT_DEFAULT_USE_HLS"),
default_hide_hls_notification: parse("LIBREDDIT_DEFAULT_HIDE_HLS"),
default_hide_awards: parse("LIBREDDIT_DEFAULT_HIDE_AWARDS"),
default_subscriptions: parse("LIBREDDIT_DEFAULT_SUBSCRIPTIONS"),
default_disable_visit_reddit_confirmation: parse("LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("LIBREDDIT_BANNER"),
robots_disable_indexing: parse("LIBREDDIT_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("LIBREDDIT_PUSHSHIFT_FRONTEND"),
}
}
}
@ -100,7 +121,11 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
"LIBREDDIT_DEFAULT_WIDE" => config.default_wide.clone(),
"LIBREDDIT_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
"LIBREDDIT_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"LIBREDDIT_BANNER" => config.banner.clone(),
"LIBREDDIT_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"LIBREDDIT_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
_ => None,
}
}
@ -113,6 +138,13 @@ pub(crate) fn get_setting(name: &str) -> Option<String> {
#[cfg(test)]
use {sealed_test::prelude::*, std::fs::write};
#[test]
fn test_deserialize() {
// Must handle empty input
let result = toml::from_str::<Config>("");
assert!(result.is_ok(), "Error: {}", result.unwrap_err());
}
#[test]
#[sealed_test(env = [("LIBREDDIT_SFW_ONLY", "on")])]
fn test_env_var() {
@ -142,3 +174,8 @@ fn test_alt_env_config_precedence() {
write("libreddit.toml", config_to_write).unwrap();
assert_eq!(get_setting("LIBREDDIT_DEFAULT_COMMENT_SORT"), Some("top".into()))
}
#[test]
#[sealed_test(env = [("LIBREDDIT_DEFAULT_SUBSCRIPTIONS", "news+bestof")])]
fn test_default_subscriptions() {
assert_eq!(get_setting("LIBREDDIT_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
}

View file

@ -3,7 +3,7 @@
use crate::client::json;
use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, setting, template, Post, Preferences};
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, template, Post, Preferences};
use askama::Template;
use hyper::{Body, Request, Response};
@ -67,11 +67,12 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
Ok(response) => {
let post = parse_post(&response[0]["data"]["children"][0]).await;
let req_url = req.uri().to_string();
// Return landing page if this post if this Reddit deems this post
// NSFW, but we have also disabled the display of NSFW content
// or if the instance is SFW-only.
if post.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) {
return Ok(nsfw_landing(req).await.unwrap_or_default());
// or if the instance is SFW-only
if post.nsfw && crate::utils::should_be_nsfw_gated(&req, &req_url) {
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
}
let filters = get_filters(&req);
@ -195,14 +196,13 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
after = response[1]["data"]["after"].as_str().unwrap_or_default().to_string();
}
}
let url = req.uri().to_string();
template(DuplicatesTemplate {
params: DuplicatesParams { before, after, sort },
post,
duplicates,
prefs: Preferences::new(&req),
url,
url: req_url,
num_posts_filtered,
all_posts_filtered,
})
@ -210,9 +210,9 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Process error.
Err(msg) => {
if msg == "quarantined" {
if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub)
quarantine(req, sub, msg)
} else {
error(req, msg).await
}

View file

@ -122,6 +122,8 @@ impl InstanceInfo {
["Deploy timestamp", &self.deploy_unix_ts.to_string()],
["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)],
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
])
.with_header_row(["Settings"]),
);
@ -139,6 +141,7 @@ impl InstanceInfo {
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
["Use HLS", &convert(&self.config.default_use_hls)],
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)],
])
.with_header_row(["Default preferences"]),
);
@ -153,10 +156,11 @@ impl InstanceInfo {
Deploy date: {}\n
Deploy timestamp: {}\n
Compile mode: {}\n
SFW only: {:?}\n
Pushshift frontend: {:?}\n
Config:\n
Banner: {:?}\n
Hide awards: {:?}\n
SFW only: {:?}\n
Default theme: {:?}\n
Default front page: {:?}\n
Default layout: {:?}\n
@ -166,15 +170,17 @@ impl InstanceInfo {
Default show NSFW: {:?}\n
Default blur NSFW: {:?}\n
Default use HLS: {:?}\n
Default hide HLS notification: {:?}\n",
Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n",
self.crate_version,
self.git_commit,
self.deploy_date,
self.deploy_unix_ts,
self.compile_mode,
self.config.sfw_only,
self.config.pushshift,
self.config.banner,
self.config.default_hide_awards,
self.config.sfw_only,
self.config.default_theme,
self.config.default_front_page,
self.config.default_layout,
@ -184,7 +190,8 @@ impl InstanceInfo {
self.config.default_show_nsfw,
self.config.default_blur_nsfw,
self.config.default_use_hls,
self.config.default_hide_hls_notification
self.config.default_hide_hls_notification,
self.config.default_subscriptions,
)
}
StringType::Html => self.to_table(),

View file

@ -161,7 +161,7 @@ async fn main() {
let mut app = server::Server::new();
// Force evaluation of statics. In instance_info case, we need to evaluate
// the timestamp so deploy date is accurate - in config case, we need to
// the timestamp so deploy date is accurate - in config case, we need to
// evaluate the configuration to avoid paying penalty at first request.
Lazy::force(&config::CONFIG);
@ -186,9 +186,21 @@ async fn main() {
app
.at("/manifest.json")
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
app
.at("/robots.txt")
.get(|_| resource("User-agent: *\nDisallow: /u/\nDisallow: /user/", "text/plain", true).boxed());
app.at("/robots.txt").get(|_| {
resource(
if match config::get_setting("LIBREDDIT_ROBOTS_DISABLE_INDEXING") {
Some(val) => val == "on",
None => false,
} {
"User-agent: *\nDisallow: /"
} else {
"User-agent: *\nDisallow: /u/\nDisallow: /user/"
},
"text/plain",
true,
)
.boxed()
});
app.at("/favicon.ico").get(|_| favicon().boxed());
app.at("/logo.png").get(|_| pwa_logo().boxed());
app.at("/Inter.var.woff2").get(|_| font().boxed());

View file

@ -1,5 +1,6 @@
// CRATES
use crate::client::json;
use crate::config::get_setting;
use crate::server::RequestExt;
use crate::subreddit::{can_access_quarantine, quarantine};
use crate::utils::{
@ -8,6 +9,8 @@ use crate::utils::{
use hyper::{Body, Request, Response};
use askama::Template;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashSet;
// STRUCTS
@ -20,13 +23,18 @@ struct PostTemplate {
prefs: Preferences,
single_thread: bool,
url: String,
url_without_query: String,
comment_query: String,
}
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap());
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Build Reddit API path
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);
let url = req.uri().to_string();
// Set sort to sort query parameter
let sort = param(&path, "sort").unwrap_or_else(|| {
@ -56,31 +64,41 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
// Parse the JSON into Post and Comment structs
let post = parse_post(&response[0]["data"]["children"][0]).await;
let req_url = req.uri().to_string();
// Return landing page if this post if this Reddit deems this post
// NSFW, but we have also disabled the display of NSFW content
// or if the instance is SFW-only.
if post.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) {
return Ok(nsfw_landing(req).await.unwrap_or_default());
if post.nsfw && crate::utils::should_be_nsfw_gated(&req, &req_url) {
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
}
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req);
let url = req.uri().to_string();
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
None => String::new(),
};
let comments = match query.as_str() {
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
};
// Use the Post and Comment structs to generate a website to show users
template(PostTemplate {
comments,
post,
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
sort,
prefs: Preferences::new(&req),
single_thread,
url,
url: req_url,
comment_query: query,
})
}
// If the Reddit API returns an error, exit and send error page to user
Err(msg) => {
if msg == "quarantined" {
if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub)
quarantine(req, sub, msg)
} else {
error(req, msg).await
}
@ -89,6 +107,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
}
// COMMENTS
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
// Parse the comment JSON into a Vector of Comments
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
@ -97,88 +116,138 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
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);
// 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, filters, req)
} else {
Vec::new()
};
let awards: Awards = Awards::parse(&data["all_awardings"]);
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;
let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" {
format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}{}\">view removed comment</a></p></div>",
post_link, id
)
} else {
rewrite_urls(&val(&comment, "body_html"))
};
let 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: val(&comment, "link_flair_text"),
background_color: val(&comment, "author_flair_background_color"),
foreground_color: val(&comment, "author_flair_text_color"),
},
distinguished: val(&comment, "distinguished"),
};
let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
// Many subreddits have a default comment posted about the sub's rules etc.
// Many libreddit users do not wish to see this kind of comment by default.
// Reddit does not tell us which users are "bots", so a good heuristic is to
// collapse stickied moderator comments.
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
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,
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,
awards,
collapsed,
is_filtered,
prefs: Preferences::new(req),
}
build_comment(&comment, data, replies, post_link, post_author, highlighted_comment, filters, req)
})
.collect()
}
fn query_comments(
json: &serde_json::Value,
post_link: &str,
post_author: &str,
highlighted_comment: &str,
filters: &HashSet<String>,
query: &str,
req: &Request<Body>,
) -> Vec<Comment> {
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
let mut results = Vec::new();
comments.into_iter().for_each(|comment| {
let data = &comment["data"];
// If this comment contains replies, handle those too
if data["replies"].is_object() {
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req))
}
let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
if c.body.to_lowercase().contains(&query.to_lowercase()) {
results.push(c);
}
});
results
}
#[allow(clippy::too_many_arguments)]
fn build_comment(
comment: &serde_json::Value,
data: &serde_json::Value,
replies: Vec<Comment>,
post_link: &str,
post_author: &str,
highlighted_comment: &str,
filters: &HashSet<String>,
req: &Request<Body>,
) -> Comment {
let id = val(comment, "id");
let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" {
format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}{}\">view removed comment</a></p></div>",
get_setting("LIBREDDIT_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
post_link,
id
)
} else {
rewrite_urls(&val(comment, "body_html"))
};
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
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);
// The JSON API only provides comments up to some threshold.
// Further comments have to be loaded by subsequent requests.
// The "kind" value will be "more" and the "count"
// shows how many more (sub-)comments exist in the respective nesting level.
// Note that in certain (seemingly random) cases, the count is simply wrong.
let more_count = data["count"].as_i64().unwrap_or_default();
let awards: Awards = Awards::parse(&data["all_awardings"]);
let parent_kind_and_id = val(comment, "parent_id");
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
let highlighted = id == highlighted_comment;
let 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: val(comment, "link_flair_text"),
background_color: val(comment, "author_flair_background_color"),
foreground_color: val(comment, "author_flair_text_color"),
},
distinguished: val(comment, "distinguished"),
};
let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
// Many subreddits have a default comment posted about the sub's rules etc.
// Many libreddit users do not wish to see this kind of comment by default.
// Reddit does not tell us which users are "bots", so a good heuristic is to
// collapse stickied moderator comments.
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
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,
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,
awards,
collapsed,
is_filtered,
more_count,
prefs: Preferences::new(req),
}
}

View file

@ -145,9 +145,9 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
})
}
Err(msg) => {
if msg == "quarantined" {
if msg == "quarantined" || msg == "gated" {
let sub = req.param("sub").unwrap_or_default();
quarantine(req, sub)
quarantine(req, sub, msg)
} else {
error(req, msg).await
}

View file

@ -253,7 +253,7 @@ impl Server {
.boxed()
}
// If there was a routing error
Err(e) => async move { new_boilerplate(def_headers, req_headers, 404, e.into()).await }.boxed(),
Err(e) => new_boilerplate(def_headers, req_headers, 404, e.into()).boxed(),
}
}))
}
@ -379,7 +379,7 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
// This loop reads the requested compressors and keeps track of whichever
// one has the highest priority per our heuristic.
for val in accept_encoding.to_string().split(',') {
for val in accept_encoding.split(',') {
let mut q: f64 = 1.0;
// The compressor and q-value (if the latter is defined)

View file

@ -19,7 +19,7 @@ struct SettingsTemplate {
// CONSTANTS
const PREFS: [&str; 12] = [
const PREFS: [&str; 13] = [
"theme",
"front_page",
"layout",
@ -32,6 +32,7 @@ const PREFS: [&str; 12] = [
"hide_hls_notification",
"autoplay_videos",
"hide_awards",
"disable_visit_reddit_confirmation",
];
// FUNCTIONS

View file

@ -97,10 +97,11 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
}
};
let req_url = req.uri().to_string();
// Return landing page if this post if this is NSFW community but the user
// has disabled the display of NSFW content or if the instance is SFW-only.
if sub.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) {
return Ok(nsfw_landing(req).await.unwrap_or_default());
if sub.nsfw && crate::utils::should_be_nsfw_gated(&req, &req_url) {
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
}
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default());
@ -144,7 +145,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
})
}
Err(msg) => match msg.as_str() {
"quarantined" => quarantine(req, sub_name),
"quarantined" | "gated" => quarantine(req, sub_name, msg),
"private" => error(req, format!("r/{} is a private community", sub_name)).await,
"banned" => error(req, format!("r/{} has been banned from Reddit", sub_name)).await,
_ => error(req, msg).await,
@ -153,9 +154,9 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
}
}
pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, String> {
pub fn quarantine(req: Request<Body>, sub: String, restriction: String) -> Result<Response<Body>, String> {
let wall = WallTemplate {
title: format!("r/{} is quarantined", sub),
title: format!("r/{} is {}", sub, restriction),
msg: "Please click the button below to continue to this subreddit.".to_string(),
url: req.uri().to_string(),
sub,
@ -323,8 +324,8 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
url,
}),
Err(msg) => {
if msg == "quarantined" {
quarantine(req, sub)
if msg == "quarantined" || msg == "gated" {
quarantine(req, sub, msg)
} else {
error(req, msg).await
}
@ -361,8 +362,8 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
url,
}),
Err(msg) => {
if msg == "quarantined" {
quarantine(req, sub)
if msg == "quarantined" || msg == "gated" {
quarantine(req, sub, msg)
} else {
error(req, msg).await
}

View file

@ -50,11 +50,12 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
// Retrieve info from user about page.
let user = user(&username).await.unwrap_or_default();
let req_url = req.uri().to_string();
// Return landing page if this post if this Reddit deems this user NSFW,
// but we have also disabled the display of NSFW content or if the instance
// is SFW-only.
if user.nsfw && (setting(&req, "show_nsfw") != "on" || crate::utils::sfw_only()) {
return Ok(nsfw_landing(req).await.unwrap_or_default());
if user.nsfw && crate::utils::should_be_nsfw_gated(&req, &req_url) {
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
}
let filters = get_filters(&req);

View file

@ -1,3 +1,4 @@
use crate::config::get_setting;
//
// CRATES
//
@ -5,6 +6,7 @@ use crate::{client::json, server::RequestExt};
use askama::Template;
use cookie::Cookie;
use hyper::{Body, Request, Response};
use once_cell::sync::Lazy;
use regex::Regex;
use rust_embed::RustEmbed;
use serde_json::Value;
@ -96,6 +98,61 @@ pub struct Author {
pub distinguished: String,
}
pub struct Poll {
pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String),
pub total_vote_count: u64,
}
impl Poll {
pub fn parse(poll_data: &Value) -> Option<Self> {
poll_data.as_object()?;
let total_vote_count = poll_data["total_vote_count"].as_u64()?;
// voting_end_timestamp is in the format of milliseconds
let voting_end_timestamp = time(poll_data["voting_end_timestamp"].as_f64()? / 1000.0);
let poll_options = PollOption::parse(&poll_data["options"])?;
Some(Self {
poll_options,
total_vote_count,
voting_end_timestamp,
})
}
pub fn most_votes(&self) -> u64 {
self.poll_options.iter().filter_map(|o| o.vote_count).max().unwrap_or(0)
}
}
pub struct PollOption {
pub id: u64,
pub text: String,
pub vote_count: Option<u64>,
}
impl PollOption {
pub fn parse(options: &Value) -> Option<Vec<Self>> {
Some(
options
.as_array()?
.iter()
.filter_map(|option| {
// For each poll option
// we can't just use as_u64() because "id": String("...") and serde would parse it as None
let id = option["id"].as_str()?.parse::<u64>().ok()?;
let text = option["text"].as_str()?.to_owned();
let vote_count = option["vote_count"].as_u64();
// Construct PollOption items
Some(Self { id, text, vote_count })
})
.collect::<Vec<Self>>(),
)
}
}
// Post flags with nsfw and stickied
pub struct Flags {
pub nsfw: bool,
@ -204,10 +261,17 @@ impl GalleryMedia {
// For each image in gallery
let media_id = item["media_id"].as_str().unwrap_or_default();
let image = &metadata[media_id]["s"];
let image_type = &metadata[media_id]["m"];
let url = if image_type == "image/gif" {
image["gif"].as_str().unwrap_or_default()
} else {
image["u"].as_str().unwrap_or_default()
};
// Construct gallery items
Self {
url: format_url(image["u"].as_str().unwrap_or_default()),
url: format_url(url),
width: image["x"].as_i64().unwrap_or_default(),
height: image["y"].as_i64().unwrap_or_default(),
caption: item["caption"].as_str().unwrap_or_default().to_string(),
@ -226,6 +290,7 @@ pub struct Post {
pub body: String,
pub author: Author,
pub permalink: String,
pub poll: Option<Poll>,
pub score: (String, String),
pub upvote_ratio: i64,
pub post_type: String,
@ -335,6 +400,7 @@ impl Post {
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
},
permalink: val(post, "permalink"),
poll: Poll::parse(&data["poll_data"]),
rel_time,
created,
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
@ -370,6 +436,7 @@ pub struct Comment {
pub awards: Awards,
pub collapsed: bool,
pub is_filtered: bool,
pub more_count: i64,
pub prefs: Preferences,
}
@ -505,6 +572,7 @@ pub struct Preferences {
pub hide_hls_notification: String,
pub use_hls: String,
pub autoplay_videos: String,
pub disable_visit_reddit_confirmation: String,
pub comment_sort: String,
pub post_sort: String,
pub subscriptions: Vec<String>,
@ -529,20 +597,21 @@ impl Preferences {
}
Self {
available_themes: themes,
theme: setting(&req, "theme"),
front_page: setting(&req, "front_page"),
layout: setting(&req, "layout"),
wide: setting(&req, "wide"),
show_nsfw: setting(&req, "show_nsfw"),
blur_nsfw: setting(&req, "blur_nsfw"),
use_hls: setting(&req, "use_hls"),
hide_hls_notification: setting(&req, "hide_hls_notification"),
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(),
filters: setting(&req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
hide_awards: setting(&req, "hide_awards"),
theme: setting(req, "theme"),
front_page: setting(req, "front_page"),
layout: setting(req, "layout"),
wide: setting(req, "wide"),
show_nsfw: setting(req, "show_nsfw"),
blur_nsfw: setting(req, "blur_nsfw"),
use_hls: setting(req, "use_hls"),
hide_hls_notification: setting(req, "hide_hls_notification"),
autoplay_videos: setting(req, "autoplay_videos"),
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
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(),
filters: setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
hide_awards: setting(req, "hide_awards"),
}
}
}
@ -590,9 +659,12 @@ pub async fn parse_post(post: &serde_json::Value) -> Post {
let permalink = val(post, "permalink");
let poll = Poll::parse(&post["data"]["poll_data"]);
let body = if val(post, "removed_by_category") == "moderator" {
format!(
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}\">view removed post</a></p></div>",
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}\">view removed post</a></p></div>",
get_setting("LIBREDDIT_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
permalink
)
} else {
@ -620,6 +692,7 @@ pub async fn parse_post(post: &serde_json::Value) -> Post {
distinguished: val(post, "distinguished"),
},
permalink,
poll,
score: format_num(score),
upvote_ratio: ratio as i64,
post_type,
@ -707,6 +780,21 @@ pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>,
}
}
static REGEX_URL_WWW: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://www\.reddit\.com/(.*)").unwrap());
static REGEX_URL_OLD: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://old\.reddit\.com/(.*)").unwrap());
static REGEX_URL_NP: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://np\.reddit\.com/(.*)").unwrap());
static REGEX_URL_PLAIN: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://reddit\.com/(.*)").unwrap());
static REGEX_URL_VIDEOS: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$|\?source=fallback))").unwrap());
static REGEX_URL_VIDEOS_HLS: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$").unwrap());
static REGEX_URL_IMAGES: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://i\.redd\.it/(.*)").unwrap());
static REGEX_URL_THUMBS_A: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://a\.thumbs\.redditmedia\.com/(.*)").unwrap());
static REGEX_URL_THUMBS_B: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://b\.thumbs\.redditmedia\.com/(.*)").unwrap());
static REGEX_URL_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://emoji\.redditmedia\.com/(.*)/(.*)").unwrap());
static REGEX_URL_PREVIEW: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://preview\.redd\.it/(.*)").unwrap());
static REGEX_URL_EXTERNAL_PREVIEW: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://external\-preview\.redd\.it/(.*)").unwrap());
static REGEX_URL_STYLES: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://styles\.redditmedia\.com/(.*)").unwrap());
static REGEX_URL_STATIC_MEDIA: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://www\.redditstatic\.com/(.*)").unwrap());
// Direct urls to proxy if proxy is enabled
pub fn format_url(url: &str) -> String {
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
@ -715,13 +803,11 @@ pub fn format_url(url: &str) -> String {
Url::parse(url).map_or(url.to_string(), |parsed| {
let domain = parsed.domain().unwrap_or_default();
let capture = |regex: &str, format: &str, segments: i16| {
Regex::new(regex).map_or(String::new(), |re| {
re.captures(url).map_or(String::new(), |caps| match segments {
1 => [format, &caps[1]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""),
_ => String::new(),
})
let capture = |regex: &Regex, format: &str, segments: i16| {
regex.captures(url).map_or(String::new(), |caps| match segments {
1 => [format, &caps[1]].join(""),
2 => [format, &caps[1], "/", &caps[2]].join(""),
_ => String::new(),
})
};
@ -747,44 +833,46 @@ pub fn format_url(url: &str) -> String {
}
match domain {
"www.reddit.com" => capture(r"https://www\.reddit\.com/(.*)", "/", 1),
"old.reddit.com" => capture(r"https://old\.reddit\.com/(.*)", "/", 1),
"np.reddit.com" => capture(r"https://np\.reddit\.com/(.*)", "/", 1),
"reddit.com" => capture(r"https://reddit\.com/(.*)", "/", 1),
"v.redd.it" => chain!(
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$|\?source=fallback))", "/vid/", 2),
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
),
"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),
"www.reddit.com" => capture(&REGEX_URL_WWW, "/", 1),
"old.reddit.com" => capture(&REGEX_URL_OLD, "/", 1),
"np.reddit.com" => capture(&REGEX_URL_NP, "/", 1),
"reddit.com" => capture(&REGEX_URL_PLAIN, "/", 1),
"v.redd.it" => chain!(capture(&REGEX_URL_VIDEOS, "/vid/", 2), capture(&REGEX_URL_VIDEOS_HLS, "/hls/", 2)),
"i.redd.it" => capture(&REGEX_URL_IMAGES, "/img/", 1),
"a.thumbs.redditmedia.com" => capture(&REGEX_URL_THUMBS_A, "/thumb/a/", 1),
"b.thumbs.redditmedia.com" => capture(&REGEX_URL_THUMBS_B, "/thumb/b/", 1),
"emoji.redditmedia.com" => capture(&REGEX_URL_EMOJI, "/emoji/", 2),
"preview.redd.it" => capture(&REGEX_URL_PREVIEW, "/preview/pre/", 1),
"external-preview.redd.it" => capture(&REGEX_URL_EXTERNAL_PREVIEW, "/preview/external-pre/", 1),
"styles.redditmedia.com" => capture(&REGEX_URL_STYLES, "/style/", 1),
"www.redditstatic.com" => capture(&REGEX_URL_STATIC_MEDIA, "/static/", 1),
_ => url.to_string(),
}
})
}
}
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#).unwrap());
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").unwrap());
// Rewrite Reddit links to Libreddit in body of text
pub fn rewrite_urls(input_text: &str) -> String {
let text1 = 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())
// Remove (html-encoded) "\" from URLs.
.replace("%5C", "")
.replace('\\', "");
let text1 =
// Rewrite Reddit links to Libreddit
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
.to_string()
// Remove (html-encoded) "\" from URLs.
.replace("%5C", "")
.replace('\\', "");
// 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
}
})
if REDDIT_PREVIEW_REGEX.is_match(&text1) {
REDDIT_PREVIEW_REGEX
.replace_all(&text1, format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
.to_string()
} else {
text1
}
}
// Format vote count to a string that will be displayed.
@ -805,20 +893,31 @@ pub fn format_num(num: i64) -> (String, String) {
// Parse a relative and absolute time from a UNIX timestamp
pub fn time(created: f64) -> (String, String) {
let time = OffsetDateTime::from_unix_timestamp(created.round() as i64).unwrap_or(OffsetDateTime::UNIX_EPOCH);
let time_delta = OffsetDateTime::now_utc() - time;
let now = OffsetDateTime::now_utc();
let min = time.min(now);
let max = time.max(now);
let time_delta = max - min;
// If the time difference is more than a month, show full date
let rel_time = if time_delta > Duration::days(30) {
let mut rel_time = if time_delta > Duration::days(30) {
time.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default()
// Otherwise, show relative date/time
} else if time_delta.whole_days() > 0 {
format!("{}d ago", time_delta.whole_days())
format!("{}d", time_delta.whole_days())
} else if time_delta.whole_hours() > 0 {
format!("{}h ago", time_delta.whole_hours())
format!("{}h", time_delta.whole_hours())
} else {
format!("{}m ago", time_delta.whole_minutes())
format!("{}m", time_delta.whole_minutes())
};
if time_delta <= Duration::days(30) {
if now < time {
rel_time += " left";
} else {
rel_time += " ago";
}
}
(
rel_time,
time
@ -883,11 +982,21 @@ pub fn sfw_only() -> bool {
}
}
// Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only();
let gate_nsfw = (setting(req, "show_nsfw") != "on") || sfw_instance;
// Nsfw landing gate should not be bypassed on a sfw only instance,
let bypass_gate = !sfw_instance && req_url.contains("&bypass_nsfw_landing");
gate_nsfw && !bypass_gate
}
/// Renders the landing page for NSFW content when the user has not enabled
/// "show NSFW posts" in settings.
pub async fn nsfw_landing(req: Request<Body>) -> Result<Response<Body>, String> {
pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Response<Body>, String> {
let res_type: ResourceType;
let url = req.uri().to_string();
// Determine from the request URL if the resource is a subreddit, a user
// page, or a post.
@ -906,7 +1015,7 @@ pub async fn nsfw_landing(req: Request<Body>) -> Result<Response<Body>, String>
res,
res_type,
prefs: Preferences::new(&req),
url,
url: req_url,
}
.render()
.unwrap_or_default();

View file

@ -4,6 +4,30 @@
:root {
--nsfw: #ff5c5d;
--admin: #ea0027;
/* Reddit redirect warning constants */
--popup-red: #ea0027;
--popup-black: #111;
--popup-text: #fff;
--popup-background-1: #0f0f0f;
--popup-background-2: #220f0f;
--popup-reddit-url: var(--popup-red);
--popup-background: repeating-linear-gradient(
-45deg,
var(--popup-background-1),
var(--popup-background-1) 50px,
var(--popup-background-2) 50px,
var(--popup-background-2) 100px
);
--popup-toreddit-background: var(--popup-black);
--popup-toreddit-text: var(--popup-red);
--popup-goback-background: var(--popup-red);
--popup-goback-text: #222;
--popup-border: 1px solid var(--popup-red);
--footer-height: 30px;
}
@font-face {
@ -26,6 +50,7 @@
--highlighted: #333;
--visited: #aaa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
--popup: #b80a27;
/* Hint color theme to browser for scrollbar */
color-scheme: dark;
@ -76,6 +101,9 @@ body {
background: var(--background);
font-size: 15px;
padding-top: 60px;
padding-bottom: var(--footer-height);
min-height: calc(100vh - 60px);
position: relative;
}
nav {
@ -119,12 +147,6 @@ nav #links svg {
display: none;
}
nav #version {
opacity: 50%;
vertical-align: -2px;
margin-right: 10px;
}
nav #libreddit {
vertical-align: -2px;
}
@ -134,10 +156,109 @@ nav #libreddit {
margin-left: 10px;
}
#reddit_link {
.popup {
display: flex;
align-items: center;
justify-content: center;
overflow: clip;
opacity: 0;
position: fixed;
width: 100vw;
height: 100vh;
bottom: 0;
right: 0;
visibility: hidden;
transition: all 0.1s ease-in-out;
z-index: 2;
}
/* fallback for firefox esr */
.popup {
background-color: #000000fd;
}
/* all other browsers */
@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
.popup {
-webkit-backdrop-filter: blur(.25rem) brightness(15%);
backdrop-filter: blur(.25rem) brightness(15%);
}
}
.popup-inner {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 600px;
max-height: 500px;
width: fit-content;
height: fit-content;
padding: 1rem;
background: var(--popup-background);
border: var(--popup-border);
border-radius: 5px;
transition: all 0.1s ease-in-out;
}
.popup-inner svg {
display: unset !important;
width: 35%;
stroke: none;
margin: 1rem;
}
.popup-inner h1 {
color: var(--popup-text);
margin: 1.5rem 1.5rem 1rem;
}
.popup-inner p {
color: var(--popup-text);
}
.popup-inner a {
border-radius: 5px;
padding: 2%;
width: 80%;
margin: 0.5rem;
cursor: pointer;
transition: all 0.1s ease-in-out;
}
#goback {
background: var(--popup-goback-background);
color: var(--popup-goback-text);
}
#goback:not(.selected):hover {
opacity: 0.8;
}
#toreddit {
background: var(--popup-toreddit-background);
color: var(--popup-toreddit-text);
border: 1px solid var(--popup-red);
}
#toreddit:not(.selected):hover {
background: var(--popup-toreddit-text);
color: var(--popup-toreddit-background);
}
.popup:target {
visibility: visible;
opacity: 1;
}
#reddit_url {
width: 80%;
color: var(--popup-reddit-url);
font-weight: 600;
line-break: anywhere;
margin-top: 1rem;
}
#code {
margin-left: 10px;
}
@ -148,6 +269,7 @@ main {
max-width: 1000px;
padding: 10px 20px;
margin: 0 auto;
padding-bottom: 4em;
}
.wide main {
@ -170,22 +292,22 @@ main {
body > footer {
display: flex;
justify-content: center;
margin: 20px;
align-items: center;
width: 100%;
background: var(--post);
position: absolute;
bottom: 0;
}
.info-button {
.footer-button {
align-items: center;
border-radius: .25rem;
box-sizing: border-box;
color: var(--text);
cursor: pointer;
display: inline-flex;
font-size: 300%;
padding: 0.5em;
}
.info-button > a:hover {
text-decoration: none;
padding-left: 1em;
opacity: 0.8;
}
/* / Body footer. */
@ -207,6 +329,7 @@ button {
background: none;
border: none;
font-weight: bold;
cursor: pointer;
}
hr {
@ -257,13 +380,17 @@ aside {
border-radius: 5px;
overflow: hidden;
}
#subreddit, #sidebar { min-width: 350px; }
#user *, #subreddit * { text-align: center; }
#user, #sub_meta, #sidebar_contents { padding: 20px; }
#sidebar, #sidebar_contents { margin-top: 10px; }
#sidebar_label { padding: 10px; }
#sidebar_label, #subreddit_label {
padding: 10px;
text-align: left;
}
#user_icon, #sub_icon {
width: 100px;
@ -275,7 +402,6 @@ aside {
}
#user_title, #sub_title {
margin: 0 20px;
font-size: 20px;
font-weight: bold;
}
@ -419,6 +545,7 @@ select, #search, #sort_options, #listing_options, #inside, #searchbox > *, #sort
select {
background: var(--outside);
transition: 0.2s background;
cursor: pointer;
}
select, #search {
@ -431,6 +558,10 @@ select, #search {
border-radius: 5px 0px 0px 5px;
}
.commentQuery {
background: var(--post);
}
#searchbox {
grid-area: searchbox;
display: flex;
@ -508,23 +639,28 @@ button.submit:hover > svg { stroke: var(--accent); }
background: transparent;
}
#commentQueryForms {
display: flex;
justify-content: space-between;
}
#allCommentsLink {
color: var(--green);
}
#sort, #search_sort {
display: flex;
align-items: center;
margin-bottom: 20px;
}
#listing_options {
overflow-x: auto;
}
#sort_options, #listing_options, main > * > footer > a {
border-radius: 5px;
align-items: center;
box-shadow: var(--shadow);
background: var(--outside);
display: flex;
overflow: hidden;
overflow-y: hidden;
}
#sort_options > a, #listing_options > a, main > * > footer > a {
@ -635,6 +771,7 @@ a.search_subreddit:hover {
"post_score post_title post_thumbnail" 1fr
"post_score post_media post_thumbnail" auto
"post_score post_body post_thumbnail" auto
"post_score post_poll post_thumbnail" auto
"post_score post_notification post_thumbnail" auto
"post_score post_footer post_thumbnail" auto
/ minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px));
@ -835,6 +972,44 @@ a.search_subreddit:hover {
overflow-wrap: anywhere;
}
.post_poll {
grid-area: post_poll;
padding: 5px 15px 5px 12px;
}
.poll_option {
position: relative;
margin-right: 15px;
margin-top: 14px;
z-index: 0;
display: flex;
align-items: center;
}
.poll_chart {
padding: 14px 0;
background-color: var(--accent);
opacity: 0.2;
border-radius: 5px;
z-index: -1;
position: absolute;
}
.poll_option span {
margin-left: 8px;
color: var(--text);
}
.poll_option span:nth-of-type(1) {
min-width: 10%;
font-weight: bold;
}
.most_voted {
opacity: 0.45;
width: 100%;
}
/* Used only for text post preview */
.post_preview {
-webkit-mask-image: linear-gradient(180deg,#000 60%,transparent);;
@ -1391,7 +1566,10 @@ td, th {
/* Mobile */
@media screen and (max-width: 800px) {
body { padding-top: 120px }
body {
padding-top: 120px;
padding-bottom: var(--footer-height);
}
main {
flex-direction: column-reverse;
@ -1430,17 +1608,21 @@ td, th {
#user, #sidebar { margin: 20px 0; }
#logo, #links { margin-bottom: 5px; }
#searchbox { width: calc(100vw - 35px); }
}
@media screen and (max-width: 480px) {
body { padding-top: 100px; }
#version { display: none; }
body {
padding-top: 100px;
padding-bottom: var(--footer-height);
}
.post {
grid-template: "post_header post_header post_thumbnail" auto
"post_title post_title post_thumbnail" 1fr
"post_media post_media post_thumbnail" auto
"post_body post_body post_thumbnail" auto
"post_poll post_poll post_thumbnail" auto
"post_notification post_notification post_thumbnail" auto
"post_score post_footer post_thumbnail" auto
/ auto 1fr fit-content(min(20%, 152px));
@ -1450,6 +1632,10 @@ td, th {
margin: 5px 0px 20px 15px;
padding: 0;
}
.post_poll {
padding: 5px 15px 10px 12px;
}
.compact .post_score { padding: 0; }
@ -1493,4 +1679,17 @@ td, th {
#post_links > li.desktop_item { display: none }
#post_links > li.mobile_item { display: auto }
.post_footer > p > span#upvoted { display: none }
.popup {
width: auto;
}
.popup-inner {
max-width: 80%;
}
#commentQueryForms {
display: initial;
justify-content: initial;
}
}

View file

@ -1,3 +1,5 @@
{% import "utils.html" as utils %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -30,17 +32,20 @@
<nav>
<div id="logo">
<a id="libreddit" href="/"><span id="lib">lib</span><span id="reddit">reddit.</span></a>
<span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span>
{% block subscriptions %}{% endblock %}
</div>
{% block search %}{% endblock %}
<div id="links">
<a id="reddit_link" href="https://www.reddit.com{{ url }}" rel="nofollow">
<a id="reddit_link" {% if prefs.disable_visit_reddit_confirmation != "on" %}href="#popup"{% else %}href="https://www.reddit.com{{ url }}" rel="nofollow"{% endif %}>
<span>reddit</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 12.0737C23 10.7308 21.9222 9.64226 20.5926 9.64226C19.9435 9.64226 19.3557 9.90274 18.923 10.3244C17.2772 9.12492 15.0099 8.35046 12.4849 8.26135L13.5814 3.05002L17.1643 3.8195C17.2081 4.73947 17.9539 5.47368 18.8757 5.47368C19.8254 5.47368 20.5951 4.69626 20.5951 3.73684C20.5951 2.77769 19.8254 2 18.8758 2C18.2001 2 17.6214 2.39712 17.3404 2.96952L13.3393 2.11066C13.2279 2.08679 13.1116 2.10858 13.016 2.17125C12.9204 2.23393 12.8533 2.33235 12.8295 2.44491L11.6051 8.25987C9.04278 8.33175 6.73904 9.10729 5.07224 10.3201C4.63988 9.90099 4.05398 9.64226 3.40757 9.64226C2.0781 9.64226 1 10.7308 1 12.0737C1 13.0618 1.58457 13.9105 2.4225 14.2909C2.38466 14.5342 2.36545 14.78 2.36505 15.0263C2.36505 18.7673 6.67626 21.8 11.9945 21.8C17.3131 21.8 21.6243 18.7673 21.6243 15.0263C21.6243 14.7794 21.6043 14.5359 21.5678 14.2957C22.4109 13.9175 23 13.0657 23 12.0737Z"/>
<path d="M22 2L12 22"/>
<path d="M2 6.70587C3.33333 8.07884 3.33333 11.5971 3.33333 11.5971M3.33333 19.647V11.5971M3.33333 11.5971C3.33333 11.5971 5.125 7.47817 8 7.47817C10.875 7.47817 12 8.85114 12 8.85114"/>
</svg>
</a>
{% if prefs.disable_visit_reddit_confirmation != "on" %}
{% call utils::visit_reddit_confirmation(url) %}
{% endif %}
<a id="settings_link" href="/settings">
<span>settings</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -48,13 +53,6 @@
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</a>
<a id="code" href="https://github.com/libreddit/libreddit" target="_blank" rel="noopener noreferrer">
<span>code</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title>code</title>
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
</svg>
</a>
</div>
</nav>
@ -65,10 +63,16 @@
{% endblock %}
</main>
{% endblock %}
<!-- FOOTER -->
{% block footer %}
<footer>
<div class="info-button">
<a href="/info" title="View instance information">&#x24D8;</a>
<p id="version">v{{ env!("CARGO_PKG_VERSION") }}</p>
<div class="footer-button">
<a href="/info" title="View instance information">ⓘ View instance info</a>
</div>
<div class="footer-button">
<a href="https://github.com/libreddit/libreddit" title="View code on GitHub">&lt;&gt; Code</a>
</div>
</footer>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% import "utils.html" as utils %}
{% if kind == "more" && parent_kind == "t1" %}
<a class="deeper_replies" href="{{ post_link }}{{ parent_id }}">&rarr; More replies</a>
<a class="deeper_replies" href="{{ post_link }}{{ parent_id }}">&rarr; More replies ({{ more_count }})</a>
{% else if kind == "t1" %}
<div id="{{ id }}" class="comment">
<div class="comment_left">
@ -35,7 +35,7 @@
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
{% endif %}
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
</blockquote>
</bockquote>
</details>
</div>
{% endif %}

View file

@ -19,10 +19,12 @@
{% if crate::utils::sfw_only() %}
This instance of Libreddit is SFW-only.</p>
{% else %}
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}.
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br>
{% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %}
{% endif %}
</p>
</div>
{% endblock %}
{% block footer %}
{% endblock %}

View file

@ -43,18 +43,32 @@
{% call utils::post(post) %}
<!-- SORT FORM -->
<div id="commentQueryForms">
<form id="sort">
<p id="comment_count">{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} <span id="sorted_by">sorted by </span></p>
<select name="sort" title="Sort comments by">
<select name="sort" title="Sort comments by" id="commentSortSelect">
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select><button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
</svg>
</button>
</form>
</select>
<button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
</svg>
</button>
</form>
<!-- SEARCH FORM -->
<form id="sort">
<input id="search" class="commentQuery" type="search" name="q" value="{{ comment_query }}" placeholder="Search comments">
<input type="hidden" name="type" value="comment">
</form>
</div>
<div>
{% if comment_query != "" %}
Comments containing "{{ comment_query }}"&nbsp;|&nbsp;<a id="allCommentsLink" href="{{ url_without_query }}">All comments</a>
{% endif %}
</div>
<!-- COMMENTS -->
{% for c in comments -%}

View file

@ -29,7 +29,7 @@
&rarr;
</svg>
</button>
</form>
</form>
{% if !is_filtered %}
{% if subreddits.len() > 0 || params.typed == "sr_user" %}
@ -99,13 +99,13 @@
{% if params.typed != "sr_user" %}
<footer>
{% if params.before != "" %}
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
<a href="?q={{ params.q|safe }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }}
&before={{ params.before }}" accesskey="P">PREV</a>
{% endif %}
{% if params.after != "" %}
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
<a href="?q={{ params.q|safe }}&restrict_sr={{ params.restrict_sr }}
&sort={{ params.sort }}&t={{ params.t }}
&after={{ params.after }}" accesskey="N">NEXT</a>
{% endif %}

View file

@ -90,6 +90,11 @@
<input type="hidden" value="off" name="hide_awards">
<input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
</div>
</fieldset>
<input id="save" type="submit" value="Save">
</div>
@ -127,7 +132,7 @@
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&blur_nsfw={{ prefs.blur_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&autoplay_videos={{ prefs.autoplay_videos }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&blur_nsfw={{ prefs.blur_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&disable_visit_reddit_confirmation={{ prefs.disable_visit_reddit_confirmation }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&autoplay_videos={{ prefs.autoplay_videos }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
</div>
</div>

View file

@ -88,7 +88,8 @@
<center>(Content from r/{{ sub.name }} has been filtered)</center>
{% endif %}
{% if !sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+") %}
<div class="panel" id="subreddit">
<details class="panel" id="subreddit" open>
<summary id="subreddit_label">Subreddit</summary>
{% if sub.wiki %}
<div id="top">
<div>Posts</div>
@ -131,7 +132,7 @@
</div>
</div>
</div>
</div>
</details>
<details class="panel" id="sidebar">
<summary id="sidebar_label">Sidebar</summary>
<div id="sidebar_contents">

View file

@ -148,6 +148,9 @@
<!-- POST BODY -->
<div class="post_body">{{ post.body|safe }}</div>
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
{% call poll(post) %}
<div class="post_footer">
<ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
@ -156,14 +159,32 @@
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
{% endif %}
<li class="desktop_item"><a href="https://reddit.com{{ post.permalink }}" rel="nofollow">reddit</a></li>
<li class="mobile_item"><a href="https://reddit.com{{ post.permalink }}" rel="nofollow">reddit</a></li>
{% call external_reddit_link(post.permalink) %}
</ul>
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
</div>
</div>
{%- endmacro %}
{% macro external_reddit_link(permalink) %}
{% for dev_type in ["desktop", "mobile"] %}
<li class="{{ dev_type }}_item">
<a
{% if prefs.disable_visit_reddit_confirmation != "on" %}
href="#popup"
{% else %}
href="https://reddit.com{{ permalink }}"
rel="nofollow"
{% endif %}
>reddit</a>
{% if prefs.disable_visit_reddit_confirmation != "on" %}
{% call visit_reddit_confirmation(permalink) %}
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% macro post_in_list(post) -%}
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
<p class="post_header">
@ -254,8 +275,64 @@
<div class="post_body post_preview">
{{ post.body|safe }}
</div>
{% call poll(post) %}
<div class="post_footer">
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}">{{ post.comments.0 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}</a>
</div>
</div>
{%- endmacro %}
{% macro visit_reddit_confirmation(url) -%}
<div class="popup" id="popup">
<div class="popup-inner">
<h1>You are about to leave Libreddit</h1>
<p>Do you want to continue?</p>
<p id="reddit_url">https://www.reddit.com{{ url }}</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 639.24 563">
<defs>
<style>.cls-1{fill:#000000;}.cls-2{fill:#f8aa00;}</style>
</defs>
<path class="cls-2" d="M322.03,0c1.95,2.5,4.88,.9,7.33,1.65,10.5,3.21,17.65,10.39,22.83,19.35,93.64,162.06,186.98,324.29,280.25,486.56,15.73,20.19,2.49,51.27-22.92,54.37-1.21,.19-2.72-.54-3.49,1.08H239.03c-70.33-2.43-141.6,.79-212.08-1.74-17.49-4.92-23.16-15.88-26.91-32.26l-.04-1.97C88.74,354.76,194.49,188.2,289.92,18.43c6.2-10.66,15.03-16.94,27.61-17.36,.95-.03,2.05,.18,2.51-1.07h2Zm-2.43,545c94.95-.02,189.9,.04,284.85-.02,11.84-.73,20.75-13.19,16.68-23.55C523.83,355.97,430.74,187.62,332.05,23.07c-7.93-9.02-22.2-6.58-27.23,3.22C230.28,156.11,155.21,285.64,80.41,415.31c-19.88,34.41-39.31,69.07-59.78,103.14-2.43,4.05-4.24,8.8-1.68,14.18,3.92,8.24,9.59,12.37,18.82,12.37,93.95,0,187.9,0,281.85,0Z"/>
<path class="cls-1" d="M319.61,545c-93.95,0-187.9,0-281.85,0-9.22,0-14.89-4.13-18.82-12.37-2.56-5.38-.75-10.13,1.68-14.18,20.47-34.07,39.9-68.73,59.78-103.14C155.21,285.64,230.28,156.11,304.82,26.29c5.03-9.8,19.3-12.24,27.23-3.22,98.7,164.55,191.79,332.9,289.1,498.35,4.06,10.36-4.85,22.82-16.68,23.55-94.94,.06-189.9,0-284.85,.02Zm.44-462.31C238.88,223.22,158.17,362.95,77.28,503h485.54c-80.94-140.13-161.61-279.79-242.77-420.31Z"/>
<path class="cls-2" d="M320.05,82.69c81.16,140.52,161.83,280.18,242.77,420.31H77.28C158.17,362.95,238.88,223.22,320.05,82.69Zm36.05,118.99c-.14-46.75-68.32-52.32-74.66-4.76,.73,51.49,9.2,102.97,12.63,154.49,1.18,13.14,10.53,21.81,23.32,22.76,13.12,.97,23.89-9.13,24.96-21.58,4.44-49.99,9.4-101.22,13.76-150.91Zm-36.56,271.4c48.8,.76,49.24-74.7-.31-75.47-53.45,3-46.02,78.12,.31,75.47Z"/>
<path class="cls-1" d="M356.1,201.67c-4.36,49.69-9.31,100.91-13.76,150.91-1.07,12.45-11.84,22.56-24.96,21.58-12.79-.95-22.14-9.63-23.31-22.76-3.43-51.52-11.9-103-12.63-154.49,6.33-47.53,74.51-42.03,74.66,4.76Z"/>
<path class="cls-1" d="M319.54,473.08c-46.34,2.64-53.75-72.47-.31-75.47,49.56,.78,49.1,76.24,.31,75.47Z"/>
</svg>
<a id="goback" href="#">No, go back!</a>
<a id="toreddit" href="https://www.reddit.com{{ url }}" rel="nofollow">Yes, take me to Reddit</a>
</div>
</div>
{%- endmacro %}
{% macro poll(post) -%}
{% match post.poll %}
{% when Some with (poll) %}
{% let widest = poll.most_votes() %}
<div class="post_poll">
<span>{{ poll.total_vote_count }} votes,</span>
<span title="{{ poll.voting_end_timestamp.1 }}">{{ poll.voting_end_timestamp.0 }}</span>
{% for option in poll.poll_options %}
<div class="poll_option">
{# Posts without vote_count (all open polls) will show up without votes.
This is an issue with Reddit API, it doesn't work on Old Reddit either. #}
{% match option.vote_count %}
{% when Some with (vote_count) %}
{% if vote_count.eq(widest) || widest == 0 %}
<div class="poll_chart most_voted"></div>
{% else %}
<div class="poll_chart" style="width: {{ (vote_count * 100) / widest }}%"></div>
{% endif %}
<span>{{ vote_count }}</span>
{% when None %}
<div class="poll_chart most_voted"></div>
<span></span>
{% endmatch %}
<span>{{ option.text }}</span>
</div>
{% endfor %}
</div>
{% when None %}
{% endmatch %}
{%- endmacro %}