Compare commits
No commits in common. "main" and "ws_stream" have entirely different histories.
@ -1 +0,0 @@
|
|||||||
target
|
|
56
.env.example
@ -1,56 +0,0 @@
|
|||||||
# Redlib configuration
|
|
||||||
# See the Configuration section of the README for a more detailed explanation of these settings.
|
|
||||||
|
|
||||||
# Instance-specific settings
|
|
||||||
# Enable SFW-only mode for the instance
|
|
||||||
REDLIB_SFW_ONLY=off
|
|
||||||
# Set a banner message for the instance
|
|
||||||
REDLIB_BANNER=
|
|
||||||
# Disable search engine indexing
|
|
||||||
REDLIB_ROBOTS_DISABLE_INDEXING=off
|
|
||||||
# Set the Pushshift frontend for "removed" links
|
|
||||||
REDLIB_PUSHSHIFT_FRONTEND=undelete.pullpush.io
|
|
||||||
|
|
||||||
# Default user settings
|
|
||||||
# Set the default theme (options: system, light, dark, black, dracula, nord, laserwave, violet, gold, rosebox, gruvboxdark, gruvboxlight)
|
|
||||||
REDLIB_DEFAULT_THEME=system
|
|
||||||
# Set the default mascot
|
|
||||||
REDLIB_DEFAULT_MASCOT=none
|
|
||||||
# Set the default front page (options: default, popular, all)
|
|
||||||
REDLIB_DEFAULT_FRONT_PAGE=default
|
|
||||||
# Set the default layout (options: card, clean, compact)
|
|
||||||
REDLIB_DEFAULT_LAYOUT=card
|
|
||||||
# Enable wide mode by default
|
|
||||||
REDLIB_DEFAULT_WIDE=off
|
|
||||||
# Set the default post sort method (options: hot, new, top, rising, controversial)
|
|
||||||
REDLIB_DEFAULT_POST_SORT=hot
|
|
||||||
# Set the default comment sort method (options: confidence, top, new, controversial, old)
|
|
||||||
REDLIB_DEFAULT_COMMENT_SORT=confidence
|
|
||||||
# Enable blurring Spoiler content by default
|
|
||||||
REDLIB_DEFAULT_BLUR_SPOILER=off
|
|
||||||
# Enable showing NSFW content by default
|
|
||||||
REDLIB_DEFAULT_SHOW_NSFW=off
|
|
||||||
# Enable blurring NSFW content by default
|
|
||||||
REDLIB_DEFAULT_BLUR_NSFW=off
|
|
||||||
# Enable HLS video format by default
|
|
||||||
REDLIB_DEFAULT_USE_HLS=off
|
|
||||||
# Enable audio+video downloads with ffmpeg.wasm
|
|
||||||
REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
|
||||||
# Hide HLS notification by default
|
|
||||||
REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
|
||||||
# Disable autoplay videos by default
|
|
||||||
REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
|
||||||
# Define a default list of subreddit subscriptions (format: sub1+sub2+sub3)
|
|
||||||
REDLIB_DEFAULT_SUBSCRIPTIONS=
|
|
||||||
# Define a default list of subreddit filters (format: sub1+sub2+sub3)
|
|
||||||
REDLIB_DEFAULT_FILTERS=
|
|
||||||
# Hide awards by default
|
|
||||||
REDLIB_DEFAULT_HIDE_AWARDS=off
|
|
||||||
# Hide sidebar and summary
|
|
||||||
REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY=off
|
|
||||||
# Disable the confirmation before visiting Reddit
|
|
||||||
REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
|
||||||
# Hide score by default
|
|
||||||
REDLIB_DEFAULT_HIDE_SCORE=off
|
|
||||||
# Enable fixed navbar by default
|
|
||||||
REDLIB_DEFAULT_FIXED_NAVBAR=on
|
|
5
.github/FUNDING.yml
vendored
@ -1,3 +1,2 @@
|
|||||||
liberapay: sigaloid
|
liberapay: spike
|
||||||
buy_me_a_coffee: sigaloid
|
custom: ['https://www.buymeacoffee.com/spikecodes']
|
||||||
github: sigaloid
|
|
||||||
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,10 +7,6 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
|
||||||
BEFORE FILING A BUG REPORT: Ensure that you are running the latest git commit. Visit /info on your instance, and ensure the git commit listed is the same commit listed on the home page.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Describe the bug
|
## Describe the bug
|
||||||
<!--
|
<!--
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
@ -35,7 +31,3 @@ Steps to reproduce the behavior:
|
|||||||
<!--
|
<!--
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
<!-- Mandatory -->
|
|
||||||
- [ ] I checked that the instance that this was reported on is running the latest git commit, or I can reproduce it locally on the latest git commit
|
|
76
.github/workflows/build-artifacts.yaml
vendored
@ -1,76 +0,0 @@
|
|||||||
name: Release Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- "*.md"
|
|
||||||
- "compose.*"
|
|
||||||
branches:
|
|
||||||
- "main"
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc
|
|
||||||
CC_aarch64_unknown_linux_musl: aarch64-linux-gnu-gcc
|
|
||||||
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-gcc
|
|
||||||
CC_armv7_unknown_linux_musleabihf: arm-linux-gnueabihf-gcc
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Rust project - latest
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
- aarch64-unknown-linux-musl
|
|
||||||
- armv7-unknown-linux-musleabihf
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
|
|
||||||
- if: matrix.target == 'x86_64-unknown-linux-musl'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --no-install-recommends musl-tools
|
|
||||||
|
|
||||||
- if: matrix.target == 'armv7-unknown-linux-musleabihf'
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y gcc-arm-linux-gnueabihf musl-tools
|
|
||||||
|
|
||||||
- if: matrix.target == 'aarch64-unknown-linux-musl'
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y gcc-aarch64-linux-gnu musl-tools
|
|
||||||
|
|
||||||
- name: Versions
|
|
||||||
id: version
|
|
||||||
run: echo "VERSION=$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --release --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Package release
|
|
||||||
run: tar czf redlib-${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release/ redlib
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.version.outputs.VERSION }}
|
|
||||||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
|
||||||
draft: true
|
|
||||||
files: |
|
|
||||||
redlib-${{ matrix.target }}.tar.gz
|
|
||||||
body: |
|
|
||||||
- ${{ github.event.head_commit.message }} ${{ github.sha }}
|
|
||||||
generate_release_notes: true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
130
.github/workflows/main-docker.yml
vendored
@ -1,109 +1,59 @@
|
|||||||
name: Container build
|
name: Docker Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
push:
|
||||||
workflows: ["Release Build"]
|
paths-ignore:
|
||||||
types:
|
- "**.md"
|
||||||
- completed
|
branches:
|
||||||
env:
|
- 'main'
|
||||||
REGISTRY_IMAGE: quay.io/redlib/redlib
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
config:
|
||||||
- { platform: linux/amd64, target: x86_64-unknown-linux-musl }
|
- { platform: 'linux/amd64', tag: 'latest', dockerfile: 'Dockerfile' }
|
||||||
- { platform: linux/arm64, target: aarch64-unknown-linux-musl }
|
- { platform: 'linux/arm64', tag: 'latest-arm', dockerfile: 'Dockerfile.arm' }
|
||||||
- { platform: linux/arm/v7, target: armv7-unknown-linux-musleabihf }
|
- { platform: 'linux/arm/v7', tag: 'latest-armv7', dockerfile: 'Dockerfile.armv7' }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY_IMAGE }}
|
|
||||||
tags: |
|
|
||||||
type=sha
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
id: buildx
|
||||||
- name: Login to Quay.io Container Registry
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: push README to Quay.io
|
||||||
|
uses: christian-korneck/update-container-description-action@v1
|
||||||
|
env:
|
||||||
|
DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
|
||||||
|
with:
|
||||||
|
destination_container_repo: quay.io/redlib/redlib
|
||||||
|
provider: quay
|
||||||
|
readme_file: 'README.md'
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ matrix.platform }}
|
file: ./${{ matrix.config.dockerfile }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
platforms: ${{ matrix.config.platform }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
push: true
|
||||||
file: Dockerfile
|
tags: quay.io/redlib/redlib:${{ matrix.config.tag }}
|
||||||
build-args: TARGET=${{ matrix.target }}
|
cache-from: type=gha
|
||||||
- name: Export digest
|
cache-to: type=gha,mode=max
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/digests
|
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
|
||||||
- name: Upload digest
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: digests-${{ matrix.target }}
|
|
||||||
path: /tmp/digests/*
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
merge:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
steps:
|
|
||||||
- name: Download digests
|
|
||||||
uses: actions/download-artifact@v4.1.7
|
|
||||||
with:
|
|
||||||
path: /tmp/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY_IMAGE }}
|
|
||||||
tags: |
|
|
||||||
type=sha
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
- name: Login to Quay.io Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: quay.io
|
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
|
||||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
|
||||||
- name: Create manifest list and push
|
|
||||||
working-directory: /tmp/digests
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
|
||||||
|
|
||||||
# - name: Push README to Quay.io
|
|
||||||
# uses: christian-korneck/update-container-description-action@v1
|
|
||||||
# env:
|
|
||||||
# DOCKER_APIKEY: ${{ secrets.APIKEY__QUAY_IO }}
|
|
||||||
# with:
|
|
||||||
# destination_container_repo: quay.io/redlib/redlib
|
|
||||||
# provider: quay
|
|
||||||
# readme_file: 'README.md'
|
|
||||||
|
|
||||||
- name: Inspect image
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
|
||||||
|
18
.github/workflows/main-rust.yml
vendored
@ -30,15 +30,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- name: Install musl-gcc
|
|
||||||
run: sudo apt-get install musl-tools
|
|
||||||
|
|
||||||
- name: Install cargo musl target
|
|
||||||
run: rustup target add x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
# Building actions
|
# Building actions
|
||||||
- name: Build
|
- name: Build
|
||||||
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-musl
|
run: RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
- name: Versions
|
- name: Versions
|
||||||
id: version
|
id: version
|
||||||
@ -51,17 +45,17 @@ jobs:
|
|||||||
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Calculate SHA512 checksum
|
- name: Calculate SHA512 checksum
|
||||||
run: sha512sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha512
|
run: sha512sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha512
|
||||||
|
|
||||||
- name: Calculate SHA256 checksum
|
- name: Calculate SHA256 checksum
|
||||||
run: sha256sum target/x86_64-unknown-linux-musl/release/redlib > redlib.sha256
|
run: sha256sum target/x86_64-unknown-linux-gnu/release/redlib > redlib.sha256
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v3
|
||||||
name: Upload a Build Artifact
|
name: Upload a Build Artifact
|
||||||
with:
|
with:
|
||||||
name: redlib
|
name: redlib
|
||||||
path: |
|
path: |
|
||||||
target/x86_64-unknown-linux-musl/release/redlib
|
target/x86_64-unknown-linux-gnu/release/redlib
|
||||||
redlib.sha512
|
redlib.sha512
|
||||||
redlib.sha256
|
redlib.sha256
|
||||||
|
|
||||||
@ -74,7 +68,7 @@ jobs:
|
|||||||
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
name: ${{ steps.version.outputs.VERSION }} - ${{ github.event.head_commit.message }}
|
||||||
draft: true
|
draft: true
|
||||||
files: |
|
files: |
|
||||||
target/x86_64-unknown-linux-musl/release/redlib
|
target/x86_64-unknown-linux-gnu/release/redlib
|
||||||
redlib.sha512
|
redlib.sha512
|
||||||
redlib.sha256
|
redlib.sha256
|
||||||
body: |
|
body: |
|
||||||
|
6
.gitignore
vendored
@ -1,10 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
redlib.toml
|
|
||||||
|
|
||||||
# Idea Files
|
# Idea Files
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# nix files
|
|
||||||
.direnv/
|
|
||||||
result
|
|
||||||
|
2
.replit
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
run = "while :; do set -ex; nix-env -iA nixpkgs.unzip; curl -o./redlib.zip -fsSL -- https://nightly.link/redlib-org/redlib/workflows/main-rust/main/redlib.zip; unzip -n redlib.zip; mv target/x86_64-unknown-linux-gnu/release/redlib .; chmod +x redlib; set +e; ./redlib -H 63115200; sleep 1; done"
|
||||||
|
language = "bash"
|
37
CREDITS
@ -2,9 +2,7 @@
|
|||||||
674Y3r <87250374+674Y3r@users.noreply.github.com>
|
674Y3r <87250374+674Y3r@users.noreply.github.com>
|
||||||
accountForIssues <52367365+accountForIssues@users.noreply.github.com>
|
accountForIssues <52367365+accountForIssues@users.noreply.github.com>
|
||||||
Adrian Lebioda <adrianlebioda@gmail.com>
|
Adrian Lebioda <adrianlebioda@gmail.com>
|
||||||
Akanksh Chitimalla <55909985+Akanksh12@users.noreply.github.com>
|
|
||||||
alefvanoon <53198048+alefvanoon@users.noreply.github.com>
|
alefvanoon <53198048+alefvanoon@users.noreply.github.com>
|
||||||
Ales Lerch <13370338+axeII@users.noreply.github.com>
|
|
||||||
Alexandre Iooss <erdnaxe@crans.org>
|
Alexandre Iooss <erdnaxe@crans.org>
|
||||||
alyaeanyx <alexandra.hollmeier@mailbox.org>
|
alyaeanyx <alexandra.hollmeier@mailbox.org>
|
||||||
AndreVuillemot160 <84594011+AndreVuillemot160@users.noreply.github.com>
|
AndreVuillemot160 <84594011+AndreVuillemot160@users.noreply.github.com>
|
||||||
@ -13,90 +11,58 @@ Artemis <51862164+artemislena@users.noreply.github.com>
|
|||||||
arthomnix <35371030+arthomnix@users.noreply.github.com>
|
arthomnix <35371030+arthomnix@users.noreply.github.com>
|
||||||
Arya K <73596856+gi-yt@users.noreply.github.com>
|
Arya K <73596856+gi-yt@users.noreply.github.com>
|
||||||
Austin Huang <im@austinhuang.me>
|
Austin Huang <im@austinhuang.me>
|
||||||
Ayaka <ayaka@kitty.community>
|
|
||||||
backfire-monism-net <development.0extl@simplelogin.com>
|
|
||||||
Basti <pred2k@users.noreply.github.com>
|
Basti <pred2k@users.noreply.github.com>
|
||||||
Ben Sherman <bennettmsherman@gmail.com>
|
|
||||||
Ben Smith <37027883+smithbm2316@users.noreply.github.com>
|
Ben Smith <37027883+smithbm2316@users.noreply.github.com>
|
||||||
beucismis <beucismis@tutamail.com>
|
|
||||||
BobIsMyManager <ahoumatt@yahoo.com>
|
BobIsMyManager <ahoumatt@yahoo.com>
|
||||||
Butter Cat <butteredcats@protonmail.com>
|
|
||||||
Butter Cat <ButteredCats@protonmail.com>
|
|
||||||
Carbrex <95964955+Carbrex@users.noreply.github.com>
|
|
||||||
ccuser44 <68124053+ccuser44@users.noreply.github.com>
|
|
||||||
Connor Holloway <c.holloway314@outlook.com>
|
|
||||||
curlpipe <11898833+curlpipe@users.noreply.github.com>
|
curlpipe <11898833+curlpipe@users.noreply.github.com>
|
||||||
dacousb <53299044+dacousb@users.noreply.github.com>
|
dacousb <53299044+dacousb@users.noreply.github.com>
|
||||||
Daniel Nathan Gray <dng@disroot.org>
|
|
||||||
Daniel Valentine <Daniel-Valentine@users.noreply.github.com>
|
Daniel Valentine <Daniel-Valentine@users.noreply.github.com>
|
||||||
Daniel Valentine <daniel@vielle.ws>
|
Daniel Valentine <daniel@vielle.ws>
|
||||||
dbrennand <52419383+dbrennand@users.noreply.github.com>
|
dbrennand <52419383+dbrennand@users.noreply.github.com>
|
||||||
Dean Sallinen <deza604@gmail.com>
|
|
||||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||||
Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com>
|
Diego Magdaleno <38844659+DiegoMagdaleno@users.noreply.github.com>
|
||||||
domve <domve@posteo.net>
|
domve <domve@posteo.net>
|
||||||
Dyras <jevwmguf@duck.com>
|
Dyras <jevwmguf@duck.com>
|
||||||
Edward <101938856+EdwardLangdon@users.noreply.github.com>
|
Edward <101938856+EdwardLangdon@users.noreply.github.com>
|
||||||
Éli Marshal <835958+EMarshal@users.noreply.github.com>
|
|
||||||
elliot <75391956+ellieeet123@users.noreply.github.com>
|
elliot <75391956+ellieeet123@users.noreply.github.com>
|
||||||
erdnaxe <erdnaxe@users.noreply.github.com>
|
erdnaxe <erdnaxe@users.noreply.github.com>
|
||||||
Esmail EL BoB <github.defilable@simplelogin.co>
|
Esmail EL BoB <github.defilable@simplelogin.co>
|
||||||
fawn <fawn@envs.net>
|
|
||||||
FireMasterK <20838718+FireMasterK@users.noreply.github.com>
|
FireMasterK <20838718+FireMasterK@users.noreply.github.com>
|
||||||
George Roubos <cowkingdom@hotmail.com>
|
George Roubos <cowkingdom@hotmail.com>
|
||||||
git-bruh <e817509a-8ee9-4332-b0ad-3a6bdf9ab63f@aleeas.com>
|
git-bruh <e817509a-8ee9-4332-b0ad-3a6bdf9ab63f@aleeas.com>
|
||||||
gmnsii <95436780+gmnsii@users.noreply.github.com>
|
gmnsii <95436780+gmnsii@users.noreply.github.com>
|
||||||
gmnsii <github.gmnsii@pm.me>
|
|
||||||
gmnsii <gmnsii@void.noreply>
|
|
||||||
Gonçalo Valério <dethos@users.noreply.github.com>
|
|
||||||
guaddy <67671414+guaddy@users.noreply.github.com>
|
guaddy <67671414+guaddy@users.noreply.github.com>
|
||||||
Harsh Mishra <erbeusgriffincasper@gmail.com>
|
Harsh Mishra <erbeusgriffincasper@gmail.com>
|
||||||
hinto.janai <hinto.janai@protonmail.com>
|
|
||||||
igna <igna@intent.cool>
|
igna <igna@intent.cool>
|
||||||
imabritishcow <bcow@protonmail.com>
|
imabritishcow <bcow@protonmail.com>
|
||||||
invakid404 <invakid404@riseup.net>
|
|
||||||
İsmail Karslı <ismail@karsli.net>
|
|
||||||
Johannes Schleifenbaum <johannes@js-webcoding.de>
|
Johannes Schleifenbaum <johannes@js-webcoding.de>
|
||||||
Jonathan Dahan <git@jonathan.is>
|
|
||||||
Josiah <70736638+fres7h@users.noreply.github.com>
|
Josiah <70736638+fres7h@users.noreply.github.com>
|
||||||
JPyke3 <pyke.jacob1@gmail.com>
|
JPyke3 <pyke.jacob1@gmail.com>
|
||||||
Kavin <20838718+FireMasterK@users.noreply.github.com>
|
Kavin <20838718+FireMasterK@users.noreply.github.com>
|
||||||
Kazi <kzshantonu@users.noreply.github.com>
|
Kazi <kzshantonu@users.noreply.github.com>
|
||||||
Kieran <42723993+EnderDev@users.noreply.github.com>
|
Kieran <42723993+EnderDev@users.noreply.github.com>
|
||||||
Kieran <kieran@dothq.co>
|
Kieran <kieran@dothq.co>
|
||||||
Kirk1984 <christoph-m@posteo.de>
|
|
||||||
kuanhulio <66286575+kuanhulio@users.noreply.github.com>
|
|
||||||
Kyle Roth <kylrth@gmail.com>
|
Kyle Roth <kylrth@gmail.com>
|
||||||
laazyCmd <laazy.pr00gramming@protonmail.com>
|
laazyCmd <laazy.pr00gramming@protonmail.com>
|
||||||
Laurențiu Nicola <lnicola@users.noreply.github.com>
|
Laurențiu Nicola <lnicola@users.noreply.github.com>
|
||||||
Lena <102762572+MarshDeer@users.noreply.github.com>
|
Lena <102762572+MarshDeer@users.noreply.github.com>
|
||||||
Leopardus <leopardus3@pm.me>
|
|
||||||
Macic <46872282+Macic-Dev@users.noreply.github.com>
|
Macic <46872282+Macic-Dev@users.noreply.github.com>
|
||||||
Mario A <10923513+Midblyte@users.noreply.github.com>
|
Mario A <10923513+Midblyte@users.noreply.github.com>
|
||||||
Márton <marton2@gmail.com>
|
|
||||||
Mathew Davies <ThePixelDeveloper@users.noreply.github.com>
|
|
||||||
Matthew Crossman <matt@crossman.page>
|
Matthew Crossman <matt@crossman.page>
|
||||||
Matthew E <matt@matthew.science>
|
Matthew E <matt@matthew.science>
|
||||||
Matthew Esposito <matt@matthew.science>
|
Matthew Esposito <matt@matthew.science>
|
||||||
Mennaruuk <52135169+Mennaruuk@users.noreply.github.com>
|
Mennaruuk <52135169+Mennaruuk@users.noreply.github.com>
|
||||||
Midou36O <midou@midou.dev>
|
|
||||||
mikupls <93015331+mikupls@users.noreply.github.com>
|
mikupls <93015331+mikupls@users.noreply.github.com>
|
||||||
Myzel394 <50424412+Myzel394@users.noreply.github.com>
|
|
||||||
Nainar <nainar.mb@gmail.com>
|
Nainar <nainar.mb@gmail.com>
|
||||||
Nathan Moos <moosingin3space@gmail.com>
|
Nathan Moos <moosingin3space@gmail.com>
|
||||||
Nazar <63452145+Tokarak@users.noreply.github.com>
|
|
||||||
Nicholas Christopher <nchristopher@tuta.io>
|
Nicholas Christopher <nchristopher@tuta.io>
|
||||||
Nick Lowery <ClockVapor@users.noreply.github.com>
|
Nick Lowery <ClockVapor@users.noreply.github.com>
|
||||||
Nico <github@dr460nf1r3.org>
|
Nico <github@dr460nf1r3.org>
|
||||||
NKIPSC <15067635+NKIPSC@users.noreply.github.com>
|
NKIPSC <15067635+NKIPSC@users.noreply.github.com>
|
||||||
nohoster <136514837+nohoster@users.noreply.github.com>
|
|
||||||
o69mar <119129086+o69mar@users.noreply.github.com>
|
o69mar <119129086+o69mar@users.noreply.github.com>
|
||||||
obeho <71698631+obeho@users.noreply.github.com>
|
obeho <71698631+obeho@users.noreply.github.com>
|
||||||
obscurity <z@x4.pm>
|
obscurity <z@x4.pm>
|
||||||
Om G <34579088+OxyMagnesium@users.noreply.github.com>
|
Om G <34579088+OxyMagnesium@users.noreply.github.com>
|
||||||
Ondřej Pešek <iTzBoboCz@users.noreply.github.com>
|
|
||||||
perennial <mail@perennialte.ch>
|
|
||||||
Peter Sawyer <petersawyer314@gmail.com>
|
|
||||||
pin <90570748+0323pin@users.noreply.github.com>
|
pin <90570748+0323pin@users.noreply.github.com>
|
||||||
potatoesAreGod <118043038+potatoesAreGod@users.noreply.github.com>
|
potatoesAreGod <118043038+potatoesAreGod@users.noreply.github.com>
|
||||||
RiversideRocks <59586759+RiversideRocks@users.noreply.github.com>
|
RiversideRocks <59586759+RiversideRocks@users.noreply.github.com>
|
||||||
@ -120,14 +86,11 @@ TheCultLeader666 <65368815+TheCultLeader666@users.noreply.github.com>
|
|||||||
TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com>
|
TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com>
|
||||||
The TwilightBlood <hwengerstickel@protonmail.com>
|
The TwilightBlood <hwengerstickel@protonmail.com>
|
||||||
tirz <36501933+tirz@users.noreply.github.com>
|
tirz <36501933+tirz@users.noreply.github.com>
|
||||||
tmak2002 <torben@tmak2002.dev>
|
|
||||||
Tokarak <63452145+Tokarak@users.noreply.github.com>
|
Tokarak <63452145+Tokarak@users.noreply.github.com>
|
||||||
Tsvetomir Bonev <invakid404@riseup.net>
|
Tsvetomir Bonev <invakid404@riseup.net>
|
||||||
Vivek <vivek@revankar.net>
|
|
||||||
Vladislav Nepogodin <nepogodin.vlad@gmail.com>
|
Vladislav Nepogodin <nepogodin.vlad@gmail.com>
|
||||||
Walkx <walkxnl@gmail.com>
|
Walkx <walkxnl@gmail.com>
|
||||||
Wichai <1482605+Chengings@users.noreply.github.com>
|
Wichai <1482605+Chengings@users.noreply.github.com>
|
||||||
wsy2220 <wsy@dogben.com>
|
wsy2220 <wsy@dogben.com>
|
||||||
xatier <xatierlike@gmail.com>
|
xatier <xatierlike@gmail.com>
|
||||||
Yaroslav Chvanov <yaroslav.chvanov@gmail.com>
|
|
||||||
Zach <72994911+zachjmurphy@users.noreply.github.com>
|
Zach <72994911+zachjmurphy@users.noreply.github.com>
|
||||||
|
1515
Cargo.lock
generated
40
Cargo.toml
@ -1,55 +1,47 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "redsunlib"
|
name = "redlib"
|
||||||
description = " Alternative private front-end to Reddit"
|
description = " Alternative private front-end to Reddit"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0"
|
||||||
repository = "https://git.stardust.wtf/iridium/redsunlib"
|
repository = "https://github.com/redlib-org/redlib"
|
||||||
version = "0.35.3"
|
version = "0.31.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Matthew Esposito <matt+cargo@matthew.science>",
|
"Matthew Esposito <matt+cargo@matthew.science>",
|
||||||
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
|
||||||
]
|
]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "redsunlib"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rinja = { version = "0.3.4", default-features = false }
|
askama = { version = "0.11.1", default-features = false }
|
||||||
cached = { version = "0.54.0", features = ["async"] }
|
cached = { version = "0.46.1", features = ["async"] }
|
||||||
clap = { version = "4.4.11", default-features = false, features = [
|
clap = { version = "4.4.11", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"env",
|
"env",
|
||||||
"derive",
|
|
||||||
] }
|
] }
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
cookie = "0.18.0"
|
cookie = "0.18.0"
|
||||||
futures-lite = "2.2.0"
|
futures-lite = "1.13.0"
|
||||||
hyper = { version = "0.14.31", features = ["full"] }
|
hyper = { version = "0.14.28", features = ["full"] }
|
||||||
|
hyper-rustls = "0.24.2"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
route-recognizer = "0.3.1"
|
route-recognizer = "0.3.1"
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.108"
|
||||||
tokio = { version = "1.35.1", features = ["full"] }
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
time = { version = "0.3.31", features = ["local-offset"] }
|
time = { version = "0.3.31", features = ["local-offset"] }
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
rust-embed = { version = "8.1.0", features = ["include-exclude"] }
|
||||||
libflate = "2.0.0"
|
libflate = "2.0.0"
|
||||||
brotli = { version = "7.0.0", features = ["std"] }
|
brotli = { version = "3.4.0", features = ["std"] }
|
||||||
toml = "0.8.8"
|
toml = "0.8.8"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
serde_yaml = "0.9.29"
|
serde_yaml = "0.9.29"
|
||||||
build_html = "2.4.0"
|
build_html = "2.4.0"
|
||||||
uuid = { version = "1.6.1", features = ["v4"] }
|
uuid = { version = "1.6.1", features = ["v4"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.21.5"
|
||||||
fastrand = "2.0.1"
|
fastrand = "2.0.1"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
rss = "2.0.7"
|
|
||||||
arc-swap = "1.7.1"
|
|
||||||
serde_json_path = "0.7.1"
|
|
||||||
async-recursion = "1.1.1"
|
|
||||||
common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] }
|
|
||||||
hyper-rustls = { version = "0.24.2", features = [ "http2" ] }
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lipsum = "0.9.0"
|
lipsum = "0.9.0"
|
||||||
@ -59,11 +51,3 @@ sealed_test = "1.0.0"
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
strip = "symbols"
|
strip = "symbols"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "redsunlib"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "scraper"
|
|
||||||
path = "src/scraper/main.rs"
|
|
||||||
|
22
Dockerfile
@ -1,17 +1,19 @@
|
|||||||
|
####################################################################################################
|
||||||
## Builder
|
## Builder
|
||||||
|
####################################################################################################
|
||||||
FROM rust:alpine AS builder
|
FROM rust:alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache musl-dev git
|
RUN apk add --no-cache musl-dev
|
||||||
|
|
||||||
WORKDIR /redsunlib
|
WORKDIR /redlib
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
## Final image
|
## Final image
|
||||||
|
####################################################################################################
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
# Import ca-certificates from builder
|
# Import ca-certificates from builder
|
||||||
@ -19,16 +21,16 @@ COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
|||||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
# Copy our build
|
# Copy our build
|
||||||
COPY --from=builder /redsunlib/target/x86_64-unknown-linux-musl/release/redsunlib /usr/local/bin/redsunlib
|
COPY --from=builder /redlib/target/x86_64-unknown-linux-musl/release/redlib /usr/local/bin/redlib
|
||||||
|
|
||||||
# Use an unprivileged user.
|
# Use an unprivileged user.
|
||||||
RUN adduser --home /nonexistent --no-create-home --disabled-password redsunlib
|
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||||
USER redsunlib
|
USER redlib
|
||||||
|
|
||||||
# Tell Docker to expose port 8080
|
# Tell Docker to expose port 8080
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Run a healthcheck every minute to make sure redsunlib is functional
|
# Run a healthcheck every minute to make sure redlib is functional
|
||||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider -q http://localhost:8080/settings || exit 1
|
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||||
|
|
||||||
CMD ["redsunlib"]
|
CMD ["redlib"]
|
45
Dockerfile.arm
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
####################################################################################################
|
||||||
|
## Builder
|
||||||
|
####################################################################################################
|
||||||
|
FROM rust:alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache g++ git
|
||||||
|
|
||||||
|
WORKDIR /usr/src/redlib
|
||||||
|
|
||||||
|
# cache dependencies in their own layer
|
||||||
|
COPY Cargo.lock Cargo.toml .
|
||||||
|
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo install --config net.git-fetch-with-cli=true --path . && rm -rf ./src
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# net.git-fetch-with-cli is specified in order to prevent a potential OOM kill
|
||||||
|
# in low memory environments. See:
|
||||||
|
# https://users.rust-lang.org/t/cargo-uses-too-much-memory-being-run-in-qemu/76531
|
||||||
|
# This is tracked under issue #641. This also requires us to install git in the
|
||||||
|
# builder.
|
||||||
|
RUN cargo install --config net.git-fetch-with-cli=true --path .
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## Final image
|
||||||
|
####################################################################################################
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Import ca-certificates from builder
|
||||||
|
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||||
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
|
# Copy our build
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/redlib /usr/local/bin/redlib
|
||||||
|
|
||||||
|
# Use an unprivileged user.
|
||||||
|
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||||
|
USER redlib
|
||||||
|
|
||||||
|
# Tell Docker to expose port 8080
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run a healthcheck every minute to make sure redlib is functional
|
||||||
|
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||||
|
|
||||||
|
CMD ["redlib"]
|
43
Dockerfile.armv7
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
####################################################################################################
|
||||||
|
## Builder
|
||||||
|
####################################################################################################
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:slim AS builder
|
||||||
|
|
||||||
|
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||||
|
ENV CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get -y install gcc-arm-linux-gnueabihf \
|
||||||
|
binutils-arm-linux-gnueabihf \
|
||||||
|
musl-tools
|
||||||
|
|
||||||
|
RUN rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
|
WORKDIR /redlib
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cargo build --target armv7-unknown-linux-musleabihf --release
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## Final image
|
||||||
|
####################################################################################################
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Import ca-certificates from builder
|
||||||
|
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||||
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
|
# Copy our build
|
||||||
|
COPY --from=builder /redlib/target/armv7-unknown-linux-musleabihf/release/redlib /usr/local/bin/redlib
|
||||||
|
|
||||||
|
# Use an unprivileged user.
|
||||||
|
RUN adduser --home /nonexistent --no-create-home --disabled-password redlib
|
||||||
|
USER redlib
|
||||||
|
|
||||||
|
# Tell Docker to expose port 8080
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run a healthcheck every minute to make sure redlib is functional
|
||||||
|
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||||
|
|
||||||
|
CMD ["redlib"]
|
411
README.md
@ -1,123 +1,92 @@
|
|||||||
<img align="left" width="128" height="128" src="https://git.stardust.wtf/attachments/842086e3-b718-4379-b718-c3a542842152" alt="logo">
|
# Redlib
|
||||||
|
|
||||||
# Redsunlib
|
> An alternative private front-end to Reddit
|
||||||
> An alternative private front-end to Reddit, a fork of [Redlib](https://github.com/redlib-org/redlib) with some <sup><sub>(minor)</sub></sup> function and cosmetic changes.
|
|
||||||
|
|
||||||
<br>
|
# ⚠️ Why do I get TOO MANY REQUESTS errors? ⚠️
|
||||||
|
#### As of July 12th, 2023, Redlib is currently not operational as Reddit's API changes, that were designed to kill third-party apps and content scrapers who don't pay [large fees](https://www.theverge.com/2023/5/31/23743993/reddit-apollo-client-api-cost), went into effect. [Read the full announcement here.](https://github.com/libreddit/libreddit/issues/840)
|
||||||
|
|
||||||
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
|
![screenshot](https://i.ibb.co/QYbqTQt/libreddit-rust.png)
|
||||||
|
|
||||||
### Disclaimer
|
|
||||||
|
|
||||||
There are rapid changes/features in this fork that can<sup>(will)</sup> change without notice. If you want to host this version, be aware that it's likely to break at some point. I still wouldn't recommend it in a production environment unless you know what you're doing. Or like living on the edge.......
|
|
||||||
|
|
||||||
> I would also like to thank the maintainers and contributors of both [Redlib](https://github.com/redlib-org/redlib) and [Libreddit](https://github.com/libreddit/libreddit) for all the work they did while I just added some low quality tacky features. ❤️
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
**10-second pitch:** Redlib is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit).
|
||||||
1. [Redsunlib](#redsunlib)
|
|
||||||
- [Disclaimer](#disclaimer)
|
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
|
||||||
2. [Table of Contents](#table-of-contents)
|
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
|
||||||
3. [Instances](#instances)
|
- 🕵 Private: all requests are proxied through the server, including media
|
||||||
4. [About](#about)
|
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
|
||||||
- [The Name](#the-name)
|
|
||||||
- [Built with](#built-with)
|
---
|
||||||
- [How is it different from other Reddit front ends?](#how-is-it-different-from-other-reddit-front-ends)
|
|
||||||
- [Teddit](#teddit)
|
I appreciate any donations! Your support allows me to continue developing Redlib.
|
||||||
- [Libreddit](#libreddit)
|
|
||||||
5. [Comparison](#comparison)
|
<a href="https://www.buymeacoffee.com/spikecodes" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px" ></a>
|
||||||
- [Speed](#speed)
|
<a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" style="height: 40px"></a>
|
||||||
- [Privacy](#privacy)
|
|
||||||
- [Reddit](#reddit)
|
|
||||||
- [Redlib](#redlib-1)
|
**Bitcoin:** `bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y`
|
||||||
- [Server](#server)
|
|
||||||
6. [Deployment](#deployment)
|
**Monero:** `45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR`
|
||||||
- [Docker](#docker)
|
|
||||||
- [Docker Compose](#docker-compose)
|
|
||||||
- [Docker CLI](#docker-cli)
|
|
||||||
- [Binary](#binary)
|
|
||||||
- [Running as a systemd service](#running-as-a-systemd-service)
|
|
||||||
- [Building from source](#building-from-source)
|
|
||||||
7. [Configuration](#configuration)
|
|
||||||
- [Instance settings](#instance-settings)
|
|
||||||
- [Default user settings](#default-user-settings)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Instances
|
# Instances
|
||||||
|
|
||||||
> [!WARNING]
|
🔗 **Want to automatically redirect Reddit links to Redlib? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
|
||||||
> 🔗 **Currently public Redsunlib instance are not available, consider using a [redlib](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) instance if you are not comfortable running your own**
|
|
||||||
|
|
||||||
You are more than welcome to host an instance and submit an issue if you want it added. That is, if you've read the [Disclaimer](#disclaimer) and it's within your "personal risk tolerance." ;)
|
[Follow this link](https://github.com/redlib-org/redlib-instances/blob/main/instances.md) for an up-to-date table of instances in Markdown format. This list is also available as [a machine-readable JSON](https://github.com/redlib-org/redlib-instances/blob/main/instances.json).
|
||||||
|
|
||||||
|
Both files are part of the [libreddit-instances](https://github.com/redlib-org/redlib-instances) repository. To contribute your [self-hosted instance](#deployment) to the list, see the [libreddit-instances README](https://github.com/redlib-org/redlib-instances/blob/main/README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# About
|
# About
|
||||||
|
|
||||||
Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
Find Redlib on 💬 [Matrix](https://matrix.to/#/#redlib:matrix.org), 🐋 [Quay.io](https://quay.io/repository/redlib/redlib), :octocat: [GitHub](https://github.com/redlib-org/redlib), and 🦊 [GitLab](https://gitlab.com/redlib/redlib).
|
||||||
|
|
||||||
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/redlib-org/redlib/issues).
|
|
||||||
|
|
||||||
## The Name
|
|
||||||
|
|
||||||
**Red sun** in the sky + Red**lib** = Redsunlib
|
|
||||||
|
|
||||||
<sup>I do self criticism constantly, because I'm trapped in a Maoist *cult* where comrades (white terrorists) criticize me merciloussly for having a fascist credit card (VISA Silver Signature Rewards) They won't let me order vegan pizza anymore because the phone is fascist and "summoning my pizza slave with bourgeois app" is "bad vibes"</sup>
|
|
||||||
|
|
||||||
## Built with
|
## Built with
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org/) - Programming language
|
- [Rust](https://www.rust-lang.org/) - Programming language
|
||||||
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
|
- [Hyper](https://github.com/hyperium/hyper) - HTTP server and client
|
||||||
- [Rinja](https://github.com/rinja-rs/rinja) - Templating engine
|
- [Askama](https://github.com/djc/askama) - Templating engine
|
||||||
- [Rustls](https://github.com/rustls/rustls) - TLS library
|
- [Rustls](https://github.com/rustls/rustls) - TLS library
|
||||||
|
|
||||||
## How is it different from other Reddit front ends?
|
## Info
|
||||||
|
Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Redlib was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||||
|
|
||||||
### Teddit
|
Redlib currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/libreddit/libreddit/issues).
|
||||||
|
|
||||||
|
## How does it compare to Teddit?
|
||||||
|
|
||||||
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Redlib into an even more polished product.
|
Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two, and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Redlib into an even more polished product.
|
||||||
|
|
||||||
If you are looking to compare, the biggest differences I have noticed are:
|
If you are looking to compare, the biggest differences I have noticed are:
|
||||||
|
|
||||||
- Redlib is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
- Redlib is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||||
- Redlib is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
|
- Redlib is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
|
||||||
|
|
||||||
### Libreddit
|
|
||||||
|
|
||||||
While originating as a fork of Libreddit, the name "Redlib" was adopted to avoid legal issues, as Reddit only allows the use of their name if structured as "XYZ For Reddit".
|
|
||||||
|
|
||||||
Several technical improvements have also been made, including:
|
|
||||||
|
|
||||||
- **OAuth token spoofing**: To circumvent rate limits imposed by Reddit, OAuth token spoofing is used to mimick the most common iOS and Android clients. While spoofing both iOS and Android clients was explored, only the Android client was chosen due to content restrictions when using an anonymous iOS client.
|
|
||||||
- **Token refreshing**: The authentication token is refreshed every 24 hours, emulating the behavior of the official Android app.
|
|
||||||
- **HTTP header mimicking**: Efforts are made to send along as many of the official app's headers as possible to reduce the likelihood of Reddit's crackdown on Redlib's requests.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Comparison
|
# Comparison
|
||||||
|
|
||||||
This section outlines how Redlib compares to Reddit in terms of speed and privacy.
|
This section outlines how Redlib compares to Reddit.
|
||||||
|
|
||||||
## Speed
|
## Speed
|
||||||
|
|
||||||
Last tested on January 12, 2024.
|
Lasted tested Nov 11, 2022.
|
||||||
|
|
||||||
Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.dev/report?url=https%3A%2F%2Fredlib.matthew.science%2F), [Reddit Report](https://pagespeed.web.dev/report?url=https://www.reddit.com)).
|
Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.dev/report?url=https%3A%2F%2Flibreddit.spike.codes%2F), [Reddit Report](https://pagespeed.web.dev/report?url=https://www.reddit.com)).
|
||||||
|
|
||||||
| Performance metric | Redlib | Reddit |
|
| | Redlib | Reddit |
|
||||||
| ------------------- | -------- | --------- |
|
|------------------------|-------------|-----------|
|
||||||
| Speed Index | 0.6s | 1.9s |
|
| Requests | 60 | 83 |
|
||||||
| Performance Score | 100% | 64% |
|
| Speed Index | 2.0s | 10.4s |
|
||||||
| Time to Interactive | **2.8s** | **12.4s** |
|
| Time to Interactive | **2.8s** | **12.4s** |
|
||||||
|
|
||||||
## Privacy
|
## Privacy
|
||||||
|
|
||||||
### Reddit
|
### Reddit
|
||||||
|
|
||||||
**Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including:
|
**Logging:** According to Reddit's [privacy policy](https://www.redditinc.com/policies/privacy-policy), they "may [automatically] log information" including:
|
||||||
|
|
||||||
- IP address
|
- IP address
|
||||||
- User-agent string
|
- User-agent string
|
||||||
- Browser type
|
- Browser type
|
||||||
@ -131,14 +100,12 @@ Results from Google PageSpeed Insights ([Redlib Report](https://pagespeed.web.de
|
|||||||
- Search terms
|
- Search terms
|
||||||
|
|
||||||
**Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
|
**Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
|
||||||
|
|
||||||
- GPS (consensual)
|
- GPS (consensual)
|
||||||
- Bluetooth (consensual)
|
- Bluetooth (consensual)
|
||||||
- Content associated with a location (consensual)
|
- Content associated with a location (consensual)
|
||||||
- Your IP Address
|
- Your IP Address
|
||||||
|
|
||||||
**Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding:
|
**Cookies:** Reddit's [cookie notice](https://www.redditinc.com/policies/cookies) documents the array of cookies used by Reddit including/regarding:
|
||||||
|
|
||||||
- Authentication
|
- Authentication
|
||||||
- Functionality
|
- Functionality
|
||||||
- Analytics and Performance
|
- Analytics and Performance
|
||||||
@ -152,93 +119,156 @@ For transparency, I hope to describe all the ways Redlib handles user privacy.
|
|||||||
|
|
||||||
#### Server
|
#### Server
|
||||||
|
|
||||||
- **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
|
* **Logging:** In production (when running the binary, hosting with docker, or using the official instances), Redlib logs nothing. When debugging (running from source without `--release`), Redlib logs post IDs fetched to aid with troubleshooting.
|
||||||
|
|
||||||
- **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://redlib.matthew.science/settings). These are not cross-site cookies and the cookies hold no personal data.
|
* **Cookies:** Redlib uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data.
|
||||||
|
|
||||||
|
#### Official instance (libreddit.spike.codes)
|
||||||
|
|
||||||
|
The official instance is hosted at https://libreddit.spike.codes.
|
||||||
|
|
||||||
|
* **Server:** The official instance runs a production binary, and thus logs nothing.
|
||||||
|
|
||||||
|
* **DNS:** The domain for the official instance uses Cloudflare as the DNS resolver. However, this site is not proxied through Cloudflare, and thus Cloudflare doesn't have access to user traffic.
|
||||||
|
|
||||||
|
* **Hosting:** The official instance is hosted on [Replit](https://replit.com/), which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models, and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## 1) Cargo
|
||||||
|
|
||||||
|
Make sure Rust stable is installed along with `cargo`, Rust's package manager.
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install libreddit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) Docker
|
||||||
|
|
||||||
|
Deploy the [Docker image](https://quay.io/repository/redlib/redlib) of Redlib:
|
||||||
|
```
|
||||||
|
docker pull quay.io/redlib/redlib
|
||||||
|
docker run -d --name redlib -p 8080:8080 quay.io/redlib/redlib
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy using a different port (in this case, port 80):
|
||||||
|
```
|
||||||
|
docker pull quay.io/redlib/redlib
|
||||||
|
docker run -d --name redlib -p 80:8080 quay.io/redlib/redlib
|
||||||
|
```
|
||||||
|
|
||||||
|
To deploy on `arm64` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-arm`.
|
||||||
|
|
||||||
|
To deploy on `armv7` platforms, simply replace `quay.io/redlib/redlib` in the commands above with `quay.io/redlib/redlib:latest-armv7`.
|
||||||
|
|
||||||
|
## 3) AUR
|
||||||
|
|
||||||
|
For ArchLinux users, Redlib is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
|
||||||
|
|
||||||
|
```
|
||||||
|
yay -S libreddit-git
|
||||||
|
```
|
||||||
|
## 4) NetBSD/pkgsrc
|
||||||
|
|
||||||
|
For NetBSD users, Redlib is available from the official repositories.
|
||||||
|
|
||||||
|
```
|
||||||
|
pkgin install libreddit
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you prefer to build from source
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /usr/pkgsrc/libreddit
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) GitHub Releases
|
||||||
|
|
||||||
|
If you're on Linux and none of these methods work for you, you can grab a Linux binary from [the newest release](https://github.com/redlib-org/redlib/releases/latest).
|
||||||
|
|
||||||
|
## 6) Replit/Heroku/Glitch
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> These are free hosting options, but they are *not* private and will monitor server usage to prevent abuse. If you need a free and easy setup, this method may work best for you.
|
||||||
|
|
||||||
|
<a href="https://repl.it/github/redlib-org/redlib"><img src="https://repl.it/badge/github/redlib-org/redlib" alt="Run on Repl.it" height="32" /></a>
|
||||||
|
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/redlib-org/redlib)
|
||||||
|
[![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button-v2.svg)](https://glitch.com/edit/#!/remix/libreddit)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
This section covers multiple ways of deploying Redlib. Using [Docker](#docker) is recommended for production.
|
Once installed, deploy Redlib to `0.0.0.0:8080` by running:
|
||||||
|
|
||||||
For configuration options, see the [Configuration section](#Configuration).
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
[Docker](https://www.docker.com) lets you run containerized applications. Containers are loosely isolated environments that are lightweight and contain everything needed to run the application, so there's no need to rely on what's installed on the host.
|
|
||||||
|
|
||||||
Docker images for Redsunlib are available at our [Gitea container registry](https://git.stardust.wtf/iridium/-/packages/container/redsunlib/latest), currently only with support for `amd64`, if you need `arm64`, or `armv7` platforms you can either build Redsunlib yourself or open an [issue](https://git.stardust.wtf/iridium/redsunlib/issues) :)
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> These instructions assume the [Compose plugin](https://docs.docker.com/compose/migrate/#what-are-the-differences-between-compose-v1-and-compose-v2) has already been installed. If not, follow these [instructions on the Docker Docs](https://docs.docker.com/compose/install) for how to do so.
|
|
||||||
|
|
||||||
Copy `compose.yaml` and modify any relevant values (for example, the ports Redlib should listen on).
|
|
||||||
|
|
||||||
Start Redlib in detached mode (running in the background):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Stream logs from the Redlib container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f redlib
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker CLI
|
|
||||||
|
|
||||||
Deploy Redlib:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
docker run -d --name redlib -p 8080:8080 git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy using a different port on the host (in this case, port 80):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
docker run -d --name redlib -p 80:8080 git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're using a reverse proxy in front of Redlib, prefix the port numbers with `127.0.0.1` so that Redlib only listens on the host port **locally**. For example, if the host port for Redlib is `8080`, specify `127.0.0.1:8080:8080`.
|
|
||||||
|
|
||||||
Stream logs from the Redlib container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f redlib
|
|
||||||
```
|
|
||||||
|
|
||||||
## Binary
|
|
||||||
|
|
||||||
Currently binaries are not supplied at this moment but will be at some point in the future but can be [built from source](#building-from-source)
|
|
||||||
|
|
||||||
Copy the binary to `/usr/bin`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp ./redlib /usr/bin/redlib
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy Redlib to `0.0.0.0:8080`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redlib
|
redlib
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!IMPORTANT]
|
## Instance settings
|
||||||
> If you're proxying Redlib through NGINX (see [issue #122](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853)), add
|
|
||||||
>
|
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
||||||
|
|
||||||
|
| Name | Possible values | Default value | Description |
|
||||||
|
|---------------------------|-----------------|------------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
|
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
|
||||||
|
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
|
||||||
|
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
|
||||||
|
| `PUSHSHIFT_FRONTEND` | String | `www.unddit.com` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
|
||||||
|
|
||||||
|
## Default User Settings
|
||||||
|
|
||||||
|
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
|
||||||
|
|
||||||
|
| Name | Possible values | Default value |
|
||||||
|
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|---------------|
|
||||||
|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight"]` | `system` |
|
||||||
|
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||||
|
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||||
|
| `WIDE` | `["on", "off"]` | `off` |
|
||||||
|
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||||
|
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||||
|
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||||
|
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
||||||
|
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||||
|
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||||
|
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
||||||
|
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
|
||||||
|
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
|
||||||
|
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
|
||||||
|
| `HIDE_SCORE` | `["on", "off"]` | `off` |
|
||||||
|
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
|
||||||
|
|
||||||
|
You can also configure Redlib with a configuration file. An example `redlib.toml` can be found below:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
REDLIB_DEFAULT_WIDE = "on"
|
||||||
|
REDLIB_DEFAULT_USE_HLS = "on"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REDLIB_DEFAULT_SHOW_NSFW=on redlib
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proxying using NGINX
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> If you're [proxying Redlib through an NGINX Reverse Proxy](https://github.com/libreddit/libreddit/issues/122#issuecomment-782226853), add
|
||||||
> ```nginx
|
> ```nginx
|
||||||
> proxy_http_version 1.1;
|
> proxy_http_version 1.1;
|
||||||
> ```
|
> ```
|
||||||
>
|
|
||||||
> to your NGINX configuration file above your `proxy_pass` line.
|
> to your NGINX configuration file above your `proxy_pass` line.
|
||||||
|
|
||||||
### Running as a systemd service
|
## systemd
|
||||||
|
|
||||||
You can use the systemd service available in `contrib/redlib.service`
|
You can use the systemd service available in `contrib/redlib.service`
|
||||||
(install it on `/etc/systemd/system/redlib.service`).
|
(install it on `/etc/systemd/system/redlib.service`).
|
||||||
@ -257,85 +287,18 @@ guarantee nginx waits for this service to start. Edit
|
|||||||
Before=nginx.service
|
Before=nginx.service
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building from source
|
## launchd
|
||||||
|
|
||||||
To deploy Redsunlib with changes not yet included in the latest release, you can build the application from source.
|
If you are on macOS, you can use the launchd service available in `contrib/redlib.plist`.
|
||||||
|
|
||||||
```bash
|
Install it with `cp contrib/redlib.plist ~/Library/LaunchAgents/`.
|
||||||
git clone https://git.stardust.wtf/iridium/redsunlib && cd redsunlib
|
|
||||||
|
Load and start it with `launchctl load ~/Library/LaunchAgents/redlib.plist`.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/redlib-org/redlib
|
||||||
|
cd redlib
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
|
|
||||||
You can configure Redlib further using environment variables. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
REDLIB_DEFAULT_SHOW_NSFW=on redlib
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
REDLIB_DEFAULT_WIDE=on REDLIB_DEFAULT_THEME=dark redlib -r
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also configure Redlib with a configuration file named `redlib.toml`. For example:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
REDLIB_DEFAULT_WIDE = "on"
|
|
||||||
REDLIB_DEFAULT_USE_HLS = "on"
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you're deploying Redlib using the **Docker CLI or Docker Compose**, environment variables can be defined in a [`.env` file](https://docs.docker.com/compose/environment-variables/set-environment-variables/), allowing you to centralize and manage configuration in one place.
|
|
||||||
>
|
|
||||||
> To configure Redlib using a `.env` file, copy the `.env.example` file to `.env` and edit it accordingly.
|
|
||||||
>
|
|
||||||
> If using the Docker CLI, add ` --env-file .env` to the command that runs Redlib. For example:
|
|
||||||
>
|
|
||||||
> ```bash
|
|
||||||
> docker run -d --name redlib -p 8080:8080 --env-file .env git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line.
|
|
||||||
|
|
||||||
## Instance settings
|
|
||||||
|
|
||||||
Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
|
||||||
|
|
||||||
| Name | Possible values | Default value | Description |
|
|
||||||
| ------------------------- | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `SFW_ONLY` | `["on", "off"]` | `off` | Enables SFW-only mode for the instance, i.e. all NSFW content is filtered. |
|
|
||||||
| `BANNER` | String | (empty) | Allows the server to set a banner to be displayed. Currently this is displayed on the instance info page. |
|
|
||||||
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
|
|
||||||
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
|
|
||||||
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
|
|
||||||
| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
|
|
||||||
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
|
|
||||||
## Default user settings
|
|
||||||
|
|
||||||
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.
|
|
||||||
|
|
||||||
| Name | Possible values | Default value |
|
|
||||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
|
||||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox", "gruvboxdark", "gruvboxlight", "tokyoNight", "catppuccin", "icebergDark", "doomone", "libredditBlack", "libredditDark", "libredditLight"]` | `system` |
|
|
||||||
| `MASCOT` | `["BoymoderBlahaj", "redsunlib" ... Add more at ./static/mascots] ` | _(none)_ |
|
|
||||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
|
||||||
| `LAYOUT` | `["card", "clean", "compact", "old", "waterfall"]` | `card` |
|
|
||||||
| `WIDE` | `["on", "off"]` | `off` |
|
|
||||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
|
||||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
|
||||||
| `BLUR_SPOILER` | `["on", "off"]` | `off` |
|
|
||||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
|
||||||
| `BLUR_NSFW` | `["on", "off"]` | `off` |
|
|
||||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
|
||||||
| `FFMPEG_VIDEO_DOWNLOADS` | `["on", "off"]` | `off` |
|
|
||||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
|
||||||
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
|
||||||
| `SUBSCRIPTIONS` | `+`-delimited list of subreddits (`sub1+sub2+sub3+...`) | _(none)_ |
|
|
||||||
| `HIDE_AWARDS` | `["on", "off"]` | `off` |
|
|
||||||
| `DISABLE_VISIT_REDDIT_CONFIRMATION` | `["on", "off"]` | `off` |
|
|
||||||
| `HIDE_SCORE` | `["on", "off"]` | `off` |
|
|
||||||
| `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` |
|
|
||||||
| `FIXED_NAVBAR` | `["on", "off"]` | `on` |
|
|
||||||
|
26
app.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "Redsunlib",
|
"name": "Redlib",
|
||||||
"description": "Private front-end for Reddit",
|
"description": "Private front-end for Reddit",
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
{
|
{
|
||||||
@ -14,9 +14,6 @@
|
|||||||
"REDLIB_DEFAULT_THEME": {
|
"REDLIB_DEFAULT_THEME": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_MASCOT": {
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"REDLIB_DEFAULT_FRONT_PAGE": {
|
"REDLIB_DEFAULT_FRONT_PAGE": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
@ -32,19 +29,13 @@
|
|||||||
"REDLIB_DEFAULT_POST_SORT": {
|
"REDLIB_DEFAULT_POST_SORT": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_BLUR_SPOILER": {
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"REDLIB_DEFAULT_SHOW_NSFW": {
|
"REDLIB_DEFAULT_SHOW_NSFW": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_BLUR_NSFW": {
|
"REDLIB_DEFAULT_BLUR_NSFW": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_USE_HLS": {
|
"REDLIB_USE_HLS": {
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS": {
|
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_HIDE_HLS_NOTIFICATION": {
|
"REDLIB_HIDE_HLS_NOTIFICATION": {
|
||||||
@ -62,26 +53,17 @@
|
|||||||
"REDLIB_BANNER": {
|
"REDLIB_BANNER": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
"REDLIB_ROBOTS_DISABLE_INDEXING": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
|
"REDLIB_DEFAULT_SUBSCRIPTIONS": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_DEFAULT_FILTERS": {
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
|
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_PUSHSHIFT_FRONTEND": {
|
"REDLIB_PUSHSHIFT_FRONTEND": {
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"REDLIB_ENABLE_RSS": {
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"REDLIB_FULL_URL": {
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
compose.yaml
@ -1,24 +0,0 @@
|
|||||||
services:
|
|
||||||
redsunlib:
|
|
||||||
image: git.stardust.wtf/iridium/redsunlib:latest
|
|
||||||
restart: always
|
|
||||||
container_name: "redsunlib"
|
|
||||||
ports:
|
|
||||||
- 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
|
|
||||||
user: nobody
|
|
||||||
read_only: true
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
# - seccomp=seccomp-redsunlib.json
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
env_file: .env
|
|
||||||
networks:
|
|
||||||
- redsunlib
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
|
|
||||||
interval: 5m
|
|
||||||
timeout: 3s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
redsunlib:
|
|
@ -1,19 +1,16 @@
|
|||||||
ADDRESS=0.0.0.0
|
ADDRESS=0.0.0.0
|
||||||
PORT=12345
|
PORT=12345
|
||||||
#REDLIB_DEFAULT_THEME=default
|
#REDLIB_DEFAULT_THEME=default
|
||||||
#REDLIB_DEFAULT_MASCOT=none
|
|
||||||
#REDLIB_DEFAULT_FRONT_PAGE=default
|
#REDLIB_DEFAULT_FRONT_PAGE=default
|
||||||
#REDLIB_DEFAULT_LAYOUT=card
|
#REDLIB_DEFAULT_LAYOUT=card
|
||||||
#REDLIB_DEFAULT_WIDE=off
|
#REDLIB_DEFAULT_WIDE=off
|
||||||
#REDLIB_DEFAULT_POST_SORT=hot
|
#REDLIB_DEFAULT_POST_SORT=hot
|
||||||
#REDLIB_DEFAULT_COMMENT_SORT=confidence
|
#REDLIB_DEFAULT_COMMENT_SORT=confidence
|
||||||
#REDLIB_DEFAULT_BLUR_SPOILER=off
|
|
||||||
#REDLIB_DEFAULT_SHOW_NSFW=off
|
#REDLIB_DEFAULT_SHOW_NSFW=off
|
||||||
#REDLIB_DEFAULT_BLUR_NSFW=off
|
#REDLIB_DEFAULT_BLUR_NSFW=off
|
||||||
#REDLIB_DEFAULT_USE_HLS=off
|
#REDLIB_DEFAULT_USE_HLS=off
|
||||||
#REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS=off
|
|
||||||
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
#REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION=off
|
||||||
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
#REDLIB_DEFAULT_AUTOPLAY_VIDEOS=off
|
||||||
#REDLIB_DEFAULT_SUBSCRIPTIONS=(sub1+sub2+sub3)
|
#REDLIB_DEFAULT_SUBSCRIPTIONS=off (sub1+sub2+sub3)
|
||||||
#REDLIB_DEFAULT_HIDE_AWARDS=off
|
#REDLIB_DEFAULT_HIDE_AWARDS=off
|
||||||
#REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
#REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION=off
|
||||||
|
@ -30,8 +30,7 @@ RestrictNamespaces=yes
|
|||||||
RestrictRealtime=yes
|
RestrictRealtime=yes
|
||||||
RestrictSUIDSGID=yes
|
RestrictSUIDSGID=yes
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
SystemCallFilter=@system-service
|
SystemCallFilter=@system-service ~@privileged ~@resources
|
||||||
SystemCallFilter=~@privileged @resources
|
|
||||||
UMask=0077
|
UMask=0077
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
# docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redsunlib:
|
web:
|
||||||
build: .
|
build: .
|
||||||
restart: always
|
restart: always
|
||||||
container_name: "redsunlib"
|
container_name: "redlib"
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080 # Specify `127.0.0.1:8080:8080` instead if using a reverse proxy
|
- 8080:8080
|
||||||
user: nobody
|
user: nobody
|
||||||
read_only: true
|
read_only: true
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
# - seccomp=seccomp-redsunlib.json
|
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
networks:
|
networks:
|
||||||
- redsunlib
|
- redlib
|
||||||
|
security_opt:
|
||||||
|
- seccomp="seccomp-redlib.json"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
|
test: ["CMD", "wget", "--spider", "-q", "--tries=1", "http://localhost:8080/settings"]
|
||||||
interval: 5m
|
interval: 5m
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
redsunlib:
|
redlib:
|
106
flake.lock
@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"crane": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1717025063,
|
|
||||||
"narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=",
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1710146030,
|
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1717112898,
|
|
||||||
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"crane": "crane",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": [
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1717121863,
|
|
||||||
"narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
71
flake.nix
@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Redlib: Private front-end for Reddit";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
|
||||||
|
|
||||||
crane = {
|
|
||||||
url = "github:ipetkov/crane";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
|
|
||||||
rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
flake-utils.follows = "flake-utils";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { nixpkgs, crane, flake-utils, rust-overlay, ... }:
|
|
||||||
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
};
|
|
||||||
|
|
||||||
inherit (pkgs) lib;
|
|
||||||
|
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
|
||||||
targets = [ "x86_64-unknown-linux-musl" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
|
||||||
|
|
||||||
|
|
||||||
src = lib.cleanSourceWith {
|
|
||||||
src = craneLib.path ./.;
|
|
||||||
filter = path: type:
|
|
||||||
(lib.hasInfix "/templates/" path) ||
|
|
||||||
(lib.hasInfix "/static/" path) ||
|
|
||||||
(craneLib.filterCargoSources path type);
|
|
||||||
};
|
|
||||||
|
|
||||||
redlib = craneLib.buildPackage {
|
|
||||||
inherit src;
|
|
||||||
strictDeps = true;
|
|
||||||
doCheck = false;
|
|
||||||
|
|
||||||
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
|
|
||||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
checks = {
|
|
||||||
my-crate = redlib;
|
|
||||||
};
|
|
||||||
|
|
||||||
packages.default = redlib;
|
|
||||||
packages.docker = pkgs.dockerTools.buildImage {
|
|
||||||
name = "quay.io/redlib/redlib";
|
|
||||||
tag = "latest";
|
|
||||||
created = "now";
|
|
||||||
copyToRoot = with pkgs.dockerTools; [ caCertificates fakeNss ];
|
|
||||||
config.Cmd = "${redlib}/bin/redlib";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
||||||
[Container]
|
|
||||||
AutoUpdate=registry
|
|
||||||
ContainerName=redlib
|
|
||||||
DropCapability=ALL
|
|
||||||
EnvironmentFile=.env
|
|
||||||
HealthCmd=["wget","--spider","-q","--tries=1","http://localhost:8080/settings"]
|
|
||||||
HealthInterval=5m
|
|
||||||
HealthTimeout=3s
|
|
||||||
Image=quay.io/redlib/redlib:latest
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PublishPort=8080:8080
|
|
||||||
ReadOnly=true
|
|
||||||
User=nobody
|
|
@ -1,31 +0,0 @@
|
|||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
base_url = "http://localhost:8080"
|
|
||||||
|
|
||||||
full_path = f"{base_url}/r/politics"
|
|
||||||
|
|
||||||
ctr = 0
|
|
||||||
|
|
||||||
def fetch_url(url):
|
|
||||||
global ctr
|
|
||||||
response = requests.get(url)
|
|
||||||
ctr += 1
|
|
||||||
print(f"Request count: {ctr}")
|
|
||||||
return response
|
|
||||||
|
|
||||||
while full_path:
|
|
||||||
response = requests.get(full_path)
|
|
||||||
ctr += 1
|
|
||||||
print(f"Request count: {ctr}")
|
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
|
||||||
comment_links = soup.find_all('a', class_='post_comments')
|
|
||||||
comment_urls = [base_url + link['href'] for link in comment_links]
|
|
||||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
||||||
executor.map(fetch_url, comment_urls)
|
|
||||||
next_link = soup.find('a', accesskey='N')
|
|
||||||
if next_link:
|
|
||||||
full_path = base_url + next_link['href']
|
|
||||||
else:
|
|
||||||
break
|
|
@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases/latest | jq -r '.tag_name')
|
|
||||||
|
|
||||||
if [[ -z "$LATEST_TAG" || "$LATEST_TAG" == "null" ]]; then
|
|
||||||
echo "Failed to fetch the latest release tag from GitHub."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
LICENSE="// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
|
||||||
// @source https://github.com/video-dev/hls.js/tree/$LATEST_TAG"
|
|
||||||
|
|
||||||
echo "$LICENSE" > ../static/hls.min.js
|
|
||||||
|
|
||||||
curl -s https://cdn.jsdelivr.net/npm/hls.js@${LATEST_TAG}/dist/hls.min.js >> ../static/hls.min.js
|
|
||||||
|
|
||||||
echo "Update complete. The latest hls.js (${LATEST_TAG}) has been saved to static/hls.min.js."
|
|
314
src/client.rs
@ -1,54 +1,34 @@
|
|||||||
use arc_swap::ArcSwap;
|
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use futures_lite::future::block_on;
|
use futures_lite::future::block_on;
|
||||||
use futures_lite::{future::Boxed, FutureExt};
|
use futures_lite::{future::Boxed, FutureExt};
|
||||||
use hyper::client::HttpConnector;
|
use hyper::client::HttpConnector;
|
||||||
use hyper::header::HeaderValue;
|
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
||||||
use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri};
|
|
||||||
use hyper_rustls::HttpsConnector;
|
use hyper_rustls::HttpsConnector;
|
||||||
use libflate::gzip;
|
use libflate::gzip;
|
||||||
use log::{error, trace, warn};
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use percent_encoding::{percent_encode, CONTROLS};
|
use percent_encoding::{percent_encode, CONTROLS};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU16};
|
|
||||||
use std::{io, result::Result};
|
use std::{io, result::Result};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::dbg_msg;
|
use crate::dbg_msg;
|
||||||
use crate::oauth::{force_refresh_token, token_daemon, Oauth};
|
use crate::oauth::{token_daemon, Oauth};
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::utils::format_url;
|
use crate::utils::format_url;
|
||||||
|
|
||||||
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||||
const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com";
|
|
||||||
|
|
||||||
const REDDIT_SHORT_URL_BASE: &str = "https://redd.it";
|
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||||
const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
|
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||||
|
client::Client::builder().build(https)
|
||||||
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
|
|
||||||
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
|
|
||||||
|
|
||||||
pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
|
|
||||||
Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
|
|
||||||
|
|
||||||
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
|
|
||||||
|
|
||||||
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
|
|
||||||
let client = block_on(Oauth::new());
|
|
||||||
tokio::spawn(token_daemon());
|
|
||||||
ArcSwap::new(client.into())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99);
|
pub static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = Lazy::new(|| {
|
||||||
|
let client = block_on(Oauth::new());
|
||||||
pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false);
|
tokio::spawn(token_daemon());
|
||||||
|
RwLock::new(client)
|
||||||
static URL_PAIRS: [(&str, &str); 2] = [
|
});
|
||||||
(ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST),
|
|
||||||
(REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Gets the canonical path for a resource on Reddit. This is accomplished by
|
/// Gets the canonical path for a resource on Reddit. This is accomplished by
|
||||||
/// making a `HEAD` request to Reddit at the path given in `path`.
|
/// making a `HEAD` request to Reddit at the path given in `path`.
|
||||||
@ -63,42 +43,20 @@ static URL_PAIRS: [(&str, &str); 2] = [
|
|||||||
/// `Location` header. An `Err(String)` is returned if Reddit responds with a
|
/// `Location` header. An `Err(String)` is returned if Reddit responds with a
|
||||||
/// 429, or if we were unable to decode the value in the `Location` header.
|
/// 429, or if we were unable to decode the value in the `Location` header.
|
||||||
#[cached(size = 1024, time = 600, result = true)]
|
#[cached(size = 1024, time = 600, result = true)]
|
||||||
#[async_recursion::async_recursion]
|
pub async fn canonical_path(path: String) -> Result<Option<String>, String> {
|
||||||
pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, String> {
|
let res = reddit_head(path.clone(), true).await?;
|
||||||
if tries == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each URL pair, try the HEAD request
|
|
||||||
let res = {
|
|
||||||
// for url base and host in URL_PAIRS, try reddit_short_head(path.clone(), true, url_base, url_base_host) and if it succeeds, set res. else, res = None
|
|
||||||
let mut res = None;
|
|
||||||
for (url_base, url_base_host) in URL_PAIRS {
|
|
||||||
res = reddit_short_head(path.clone(), true, url_base, url_base_host).await.ok();
|
|
||||||
if let Some(res) = &res {
|
|
||||||
if !res.status().is_client_error() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = res.ok_or_else(|| "Unable to make HEAD request to Reddit.".to_string())?;
|
|
||||||
let status = res.status().as_u16();
|
let status = res.status().as_u16();
|
||||||
let policy_error = res.headers().get(header::RETRY_AFTER).is_some();
|
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
|
429 => Err("Too many requests.".to_string()),
|
||||||
|
|
||||||
// If Reddit responds with a 2xx, then the path is already canonical.
|
// If Reddit responds with a 2xx, then the path is already canonical.
|
||||||
200..=299 => Ok(Some(path)),
|
200..=299 => Ok(Some(path)),
|
||||||
|
|
||||||
// If Reddit responds with a 301, then the path is redirected.
|
// If Reddit responds with a 301, then the path is redirected.
|
||||||
301 => match res.headers().get(header::LOCATION) {
|
301 => match res.headers().get(header::LOCATION) {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
let Ok(original) = val.to_str() else {
|
let original = val.to_str().unwrap();
|
||||||
return Err("Unable to decode Location header.".to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
// We need to strip the .json suffix from the original path.
|
// We need to strip the .json suffix from the original path.
|
||||||
// In addition, we want to remove share parameters.
|
// In addition, we want to remove share parameters.
|
||||||
// Cut it off here instead of letting it propagate all the way
|
// Cut it off here instead of letting it propagate all the way
|
||||||
@ -111,9 +69,7 @@ pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, S
|
|||||||
// also remove all Reddit domain parts with format_url.
|
// also remove all Reddit domain parts with format_url.
|
||||||
// Otherwise, it will literally redirect to Reddit.com.
|
// Otherwise, it will literally redirect to Reddit.com.
|
||||||
let uri = format_url(stripped_uri);
|
let uri = format_url(stripped_uri);
|
||||||
|
Ok(Some(uri))
|
||||||
// Decrement tries and try again
|
|
||||||
canonical_path(uri, tries - 1).await
|
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
},
|
},
|
||||||
@ -122,12 +78,6 @@ pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, S
|
|||||||
// as above), return a None.
|
// as above), return a None.
|
||||||
300..=399 => Ok(None),
|
300..=399 => Ok(None),
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
429 => Err("Too many requests.".to_string()),
|
|
||||||
|
|
||||||
// Special condition rate limiting - https://github.com/redlib-org/redlib/issues/229
|
|
||||||
403 if policy_error => Err("Too many requests.".to_string()),
|
|
||||||
|
|
||||||
_ => Ok(
|
_ => Ok(
|
||||||
res
|
res
|
||||||
.headers()
|
.headers()
|
||||||
@ -138,12 +88,12 @@ pub async fn canonical_path(path: String, tries: i8) -> Result<Option<String>, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
||||||
let mut url = format!("{format}?{}", req.uri().query().unwrap_or_default());
|
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
||||||
|
|
||||||
// For each parameter in request
|
// For each parameter in request
|
||||||
for (name, value) in &req.params() {
|
for (name, value) in req.params().iter() {
|
||||||
// Fill the parameter value in the url
|
// Fill the parameter value in the url
|
||||||
url = url.replace(&format!("{{{name}}}"), value);
|
url = url.replace(&format!("{{{}}}", name), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream(&url, &req).await
|
stream(&url, &req).await
|
||||||
@ -151,12 +101,12 @@ pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, S
|
|||||||
|
|
||||||
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
|
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// First parameter is target URL (mandatory).
|
// First parameter is target URL (mandatory).
|
||||||
let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
|
let uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
|
||||||
|
|
||||||
// Build the hyper client from the HTTPS connector.
|
// Build the hyper client from the HTTPS connector.
|
||||||
let client: &Lazy<Client<_, Body>> = &CLIENT;
|
let client: client::Client<_, hyper::Body> = CLIENT.clone();
|
||||||
|
|
||||||
let mut builder = Request::get(parsed_uri);
|
let mut builder = Request::get(uri);
|
||||||
|
|
||||||
// Copy useful headers from original request
|
// Copy useful headers from original request
|
||||||
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
||||||
@ -194,32 +144,26 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
|
|||||||
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
|
/// Makes a GET request to Reddit at `path`. By default, this will honor HTTP
|
||||||
/// 3xx codes Reddit returns and will automatically redirect.
|
/// 3xx codes Reddit returns and will automatically redirect.
|
||||||
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
fn reddit_get(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||||
request(&Method::GET, path, true, quarantine, REDDIT_URL_BASE, REDDIT_URL_BASE_HOST)
|
request(&Method::GET, path, true, quarantine)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a HEAD request to Reddit at `path, using the short URL base. This will not follow redirects.
|
/// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
||||||
fn reddit_short_head(path: String, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||||
request(&Method::HEAD, path, false, quarantine, base_path, host)
|
request(&Method::HEAD, path, false, quarantine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Makes a HEAD request to Reddit at `path`. This will not follow redirects.
|
/// Makes a request to Reddit. If `redirect` is `true`, request_with_redirect
|
||||||
// fn reddit_head(path: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
|
||||||
// request(&Method::HEAD, path, false, quarantine, false)
|
|
||||||
// }
|
|
||||||
// Unused - reddit_head is only ever called in the context of a short URL
|
|
||||||
|
|
||||||
/// Makes a request to Reddit. If `redirect` is `true`, `request_with_redirect`
|
|
||||||
/// will recurse on the URL that Reddit provides in the Location HTTP header
|
/// will recurse on the URL that Reddit provides in the Location HTTP header
|
||||||
/// in its response.
|
/// in its response.
|
||||||
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool, base_path: &'static str, host: &'static str) -> Boxed<Result<Response<Body>, String>> {
|
fn request(method: &'static Method, path: String, redirect: bool, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
|
||||||
// Build Reddit URL from path.
|
// Build Reddit URL from path.
|
||||||
let url = format!("{base_path}{path}");
|
let url = format!("{}{}", REDDIT_URL_BASE, path);
|
||||||
|
|
||||||
// Construct the hyper client from the HTTPS connector.
|
// Construct the hyper client from the HTTPS connector.
|
||||||
let client: &Lazy<Client<_, Body>> = &CLIENT;
|
let client: client::Client<_, hyper::Body> = CLIENT.clone();
|
||||||
|
|
||||||
let (token, vendor_id, device_id, user_agent, loid) = {
|
let (token, vendor_id, device_id, user_agent, loid) = {
|
||||||
let client = OAUTH_CLIENT.load_full();
|
let client = block_on(OAUTH_CLIENT.read());
|
||||||
(
|
(
|
||||||
client.token.clone(),
|
client.token.clone(),
|
||||||
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
|
client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(),
|
||||||
@ -228,40 +172,29 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
|
client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build request to Reddit. When making a GET, request gzip compression.
|
// Build request to Reddit. When making a GET, request gzip compression.
|
||||||
// (Reddit doesn't do brotli yet.)
|
// (Reddit doesn't do brotli yet.)
|
||||||
let mut headers = vec![
|
let builder = Request::builder()
|
||||||
("User-Agent", user_agent),
|
.method(method)
|
||||||
("Client-Vendor-Id", vendor_id),
|
.uri(&url)
|
||||||
("X-Reddit-Device-Id", device_id),
|
.header("User-Agent", user_agent)
|
||||||
("x-reddit-loid", loid),
|
.header("Client-Vendor-Id", vendor_id)
|
||||||
("Host", host.to_string()),
|
.header("X-Reddit-Device-Id", device_id)
|
||||||
("Authorization", format!("Bearer {token}")),
|
.header("x-reddit-loid", loid)
|
||||||
("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }),
|
.header("Host", "oauth.reddit.com")
|
||||||
(
|
.header("Authorization", &format!("Bearer {}", token))
|
||||||
|
.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",
|
"Cookie",
|
||||||
if quarantine {
|
if quarantine {
|
||||||
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D".into()
|
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D"
|
||||||
} else {
|
} else {
|
||||||
"".into()
|
""
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
("X-Reddit-Width", fastrand::u32(300..500).to_string()),
|
.body(Body::empty());
|
||||||
("X-Reddit-DPR", "2".to_owned()),
|
|
||||||
("Device-Name", format!("Android {}", fastrand::u8(9..=14))),
|
|
||||||
];
|
|
||||||
|
|
||||||
// shuffle headers: https://github.com/redlib-org/redlib/issues/324
|
|
||||||
fastrand::shuffle(&mut headers);
|
|
||||||
|
|
||||||
let mut builder = Request::builder().method(method).uri(&url);
|
|
||||||
|
|
||||||
for (key, value) in headers {
|
|
||||||
builder = builder.header(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let builder = builder.body(Body::empty());
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
match builder {
|
match builder {
|
||||||
@ -273,13 +206,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
if !redirect {
|
if !redirect {
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
};
|
};
|
||||||
let location_header = response.headers().get(header::LOCATION);
|
|
||||||
if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) {
|
|
||||||
return Err("Reddit response was invalid".to_string());
|
|
||||||
}
|
|
||||||
return request(
|
return request(
|
||||||
method,
|
method,
|
||||||
location_header
|
response
|
||||||
|
.headers()
|
||||||
|
.get(header::LOCATION)
|
||||||
.map(|val| {
|
.map(|val| {
|
||||||
// We need to make adjustments to the URI
|
// We need to make adjustments to the URI
|
||||||
// we get back from Reddit. Namely, we
|
// we get back from Reddit. Namely, we
|
||||||
@ -292,19 +224,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
// required.
|
// required.
|
||||||
//
|
//
|
||||||
// 2. Percent-encode the path.
|
// 2. Percent-encode the path.
|
||||||
let new_path = percent_encode(val.as_bytes(), CONTROLS)
|
let new_path = percent_encode(val.as_bytes(), CONTROLS).to_string().trim_start_matches(REDDIT_URL_BASE).to_string();
|
||||||
.to_string()
|
format!("{}{}raw_json=1", new_path, if new_path.contains('?') { "&" } else { "?" })
|
||||||
.trim_start_matches(REDDIT_URL_BASE)
|
|
||||||
.trim_start_matches(ALTERNATIVE_REDDIT_URL_BASE)
|
|
||||||
.to_string();
|
|
||||||
format!("{new_path}{}raw_json=1", if new_path.contains('?') { "&" } else { "?" })
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
true,
|
true,
|
||||||
quarantine,
|
quarantine,
|
||||||
base_path,
|
|
||||||
host,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
@ -357,7 +283,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
dbg_msg!("{method} {REDDIT_URL_BASE}{path}: {}", e);
|
dbg_msg!("{} {}: {}", method, path, e);
|
||||||
|
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
@ -372,124 +298,59 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
#[cached(size = 100, time = 30, result = true)]
|
#[cached(size = 100, time = 30, result = true)]
|
||||||
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
|
||||||
// Closure to quickly build errors
|
// Closure to quickly build errors
|
||||||
let err = |msg: &str, e: String, path: String| -> Result<Value, String> {
|
let err = |msg: &str, e: String| -> Result<Value, String> {
|
||||||
// eprintln!("{} - {}: {}", url, msg, e);
|
// eprintln!("{} - {}: {}", url, msg, e);
|
||||||
Err(format!("{msg}: {e} | {path}"))
|
Err(format!("{}: {}", msg, e))
|
||||||
};
|
};
|
||||||
|
|
||||||
// First, handle rolling over the OAUTH_CLIENT if need be.
|
|
||||||
let current_rate_limit = OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst);
|
|
||||||
let is_rolling_over = OAUTH_IS_ROLLING_OVER.load(Ordering::SeqCst);
|
|
||||||
if current_rate_limit < 10 && !is_rolling_over {
|
|
||||||
warn!("Rate limit {current_rate_limit} is low. Spawning force_refresh_token()");
|
|
||||||
tokio::spawn(force_refresh_token());
|
|
||||||
}
|
|
||||||
OAUTH_RATELIMIT_REMAINING.fetch_sub(1, Ordering::SeqCst);
|
|
||||||
|
|
||||||
// Fetch the url...
|
// Fetch the url...
|
||||||
match reddit_get(path.clone(), quarantine).await {
|
match reddit_get(path.clone(), quarantine).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
|
|
||||||
let reset: Option<String> = if let (Some(remaining), Some(reset), Some(used)) = (
|
|
||||||
response.headers().get("x-ratelimit-remaining").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
|
||||||
response.headers().get("x-ratelimit-reset").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
|
||||||
response.headers().get("x-ratelimit-used").and_then(|val| val.to_str().ok().map(|s| s.to_string())),
|
|
||||||
) {
|
|
||||||
trace!(
|
|
||||||
"Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}",
|
|
||||||
if is_rolling_over { "yes" } else { "no" },
|
|
||||||
);
|
|
||||||
Some(reset)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// asynchronously aggregate the chunks of the body
|
// asynchronously aggregate the chunks of the body
|
||||||
match hyper::body::aggregate(response).await {
|
match hyper::body::aggregate(response).await {
|
||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
let has_remaining = body.has_remaining();
|
|
||||||
|
|
||||||
if !has_remaining {
|
|
||||||
// Rate limited, so spawn a force_refresh_token()
|
|
||||||
tokio::spawn(force_refresh_token());
|
|
||||||
return match reset {
|
|
||||||
Some(val) => Err(format!(
|
|
||||||
"Reddit rate limit exceeded. Try refreshing in a few seconds.\
|
|
||||||
Rate limit will reset in: {val}"
|
|
||||||
)),
|
|
||||||
None => Err("Reddit rate limit exceeded".to_string()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the response from Reddit as JSON
|
// Parse the response from Reddit as JSON
|
||||||
match serde_json::from_reader(body.reader()) {
|
match serde_json::from_reader(body.reader()) {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
let json: Value = value;
|
let json: Value = value;
|
||||||
|
|
||||||
// If user is suspended
|
|
||||||
if let Some(data) = json.get("data") {
|
|
||||||
if let Some(is_suspended) = data.get("is_suspended").and_then(Value::as_bool) {
|
|
||||||
if is_suspended {
|
|
||||||
return Err("suspended".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Reddit returned an error
|
// If Reddit returned an error
|
||||||
if json["error"].is_i64() {
|
if json["error"].is_i64() {
|
||||||
// OAuth token has expired; http status 401
|
Err(
|
||||||
if json["message"] == "Unauthorized" {
|
json["reason"]
|
||||||
error!("Forcing a token refresh");
|
.as_str()
|
||||||
let () = force_refresh_token().await;
|
.unwrap_or_else(|| {
|
||||||
return Err("OAuth token has expired. Please refresh the page!".to_string());
|
json["message"].as_str().unwrap_or_else(|| {
|
||||||
}
|
eprintln!("{}{} - Error parsing reddit error", REDDIT_URL_BASE, path);
|
||||||
|
"Error parsing reddit error"
|
||||||
// Handle quarantined
|
})
|
||||||
if json["reason"] == "quarantined" {
|
})
|
||||||
return Err("quarantined".into());
|
.to_string(),
|
||||||
}
|
)
|
||||||
// Handle gated
|
|
||||||
if json["reason"] == "gated" {
|
|
||||||
return Err("gated".into());
|
|
||||||
}
|
|
||||||
// Handle private subs
|
|
||||||
if json["reason"] == "private" {
|
|
||||||
return Err("private".into());
|
|
||||||
}
|
|
||||||
// Handle banned subs
|
|
||||||
if json["reason"] == "banned" {
|
|
||||||
return Err("banned".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("Reddit error {} \"{}\": {} | {path}", json["error"], json["reason"], json["message"]))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(json)
|
Ok(json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Got an invalid response from reddit {e}. Status code: {status}");
|
|
||||||
if status.is_server_error() {
|
if status.is_server_error() {
|
||||||
Err("Reddit is having issues, check if there's an outage".to_string())
|
Err("Reddit is having issues, check if there's an outage".to_string())
|
||||||
} else {
|
} else {
|
||||||
err("Failed to parse page JSON data", e.to_string(), path)
|
err("Failed to parse page JSON data", e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => err("Failed receiving body from Reddit", e.to_string(), path),
|
Err(e) => err("Failed receiving body from Reddit", e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => err("Couldn't send request to Reddit", e, path),
|
Err(e) => err("Couldn't send request to Reddit", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL";
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_localization_popular() {
|
async fn test_localization_popular() {
|
||||||
let val = json(POPULAR_URL.to_string(), false).await.unwrap();
|
let val = json("/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL".to_string(), false).await.unwrap();
|
||||||
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
|
assert_eq!("GLOBAL", val["data"]["geo_filter"].as_str().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,34 +358,13 @@ async fn test_localization_popular() {
|
|||||||
async fn test_obfuscated_share_link() {
|
async fn test_obfuscated_share_link() {
|
||||||
let share_link = "/r/rust/s/kPgq8WNHRK".into();
|
let share_link = "/r/rust/s/kPgq8WNHRK".into();
|
||||||
// Correct link without share parameters
|
// Correct link without share parameters
|
||||||
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc/".into();
|
let canonical_link = "/r/rust/comments/18t5968/why_use_tuple_struct_over_standard_struct/kfbqlbc".into();
|
||||||
assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link)));
|
assert_eq!(canonical_path(share_link).await, Ok(Some(canonical_link)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_share_link_strip_json() {
|
async fn test_share_link_strip_json() {
|
||||||
let link = "/17krzvz".into();
|
let link = "/17krzvz".into();
|
||||||
let canonical_link = "/comments/17krzvz".into();
|
let canonical_link = "/r/nfl/comments/17krzvz/rapoport_sources_former_no_2_overall_pick/".into();
|
||||||
assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link)));
|
assert_eq!(canonical_path(link).await, Ok(Some(canonical_link)));
|
||||||
}
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_private_sub() {
|
|
||||||
let link = json("/r/suicide/about.json?raw_json=1".into(), true).await;
|
|
||||||
assert!(link.is_err());
|
|
||||||
assert_eq!(link, Err("private".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_banned_sub() {
|
|
||||||
let link = json("/r/aaa/about.json?raw_json=1".into(), true).await;
|
|
||||||
assert!(link.is_err());
|
|
||||||
assert_eq!(link, Err("banned".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_gated_sub() {
|
|
||||||
// quarantine to false to specifically catch when we _don't_ catch it
|
|
||||||
let link = json("/r/drugs/about.json?raw_json=1".into(), false).await;
|
|
||||||
assert!(link.is_err());
|
|
||||||
assert_eq!(link, Err("gated".into()));
|
|
||||||
}
|
}
|
||||||
|
@ -9,15 +9,15 @@ use std::{env::var, fs::read_to_string};
|
|||||||
// first request) and contains the instance settings.
|
// first request) and contains the instance settings.
|
||||||
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
|
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
|
||||||
|
|
||||||
// This serves as the frontend for an archival API - on removed comments, this URL
|
// This serves as the frontend for the Pushshift API - on removed comments, this URL will
|
||||||
// will be the base of a link, to display removed content (on another site).
|
// be the base of a link, to display removed content (on another site).
|
||||||
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "undelete.pullpush.io";
|
pub const DEFAULT_PUSHSHIFT_FRONTEND: &str = "www.unddit.com";
|
||||||
|
|
||||||
/// Stores the configuration parsed from the environment variables and the
|
/// Stores the configuration parsed from the environment variables and the
|
||||||
/// config file. `Config::Default()` contains None for each setting.
|
/// config file. `Config::Default()` contains None for each setting.
|
||||||
/// When adding more config settings, add it to `Config::load`,
|
/// When adding more config settings, add it to `Config::load`,
|
||||||
/// `get_setting_from_config`, both below, as well as
|
/// `get_setting_from_config`, both below, as well as
|
||||||
/// `instance_info::InstanceInfo.to_string`(), README.md and app.json.
|
/// instance_info::InstanceInfo.to_string(), README.md and app.json.
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(rename = "REDLIB_SFW_ONLY")]
|
#[serde(rename = "REDLIB_SFW_ONLY")]
|
||||||
@ -28,10 +28,6 @@ pub struct Config {
|
|||||||
#[serde(alias = "LIBREDDIT_DEFAULT_THEME")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_THEME")]
|
||||||
pub(crate) default_theme: Option<String>,
|
pub(crate) default_theme: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_MASCOT")]
|
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_MASCOT")]
|
|
||||||
pub(crate) default_mascot: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_FRONT_PAGE")]
|
#[serde(rename = "REDLIB_DEFAULT_FRONT_PAGE")]
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_FRONT_PAGE")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_FRONT_PAGE")]
|
||||||
pub(crate) default_front_page: Option<String>,
|
pub(crate) default_front_page: Option<String>,
|
||||||
@ -52,10 +48,6 @@ pub struct Config {
|
|||||||
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_POST_SORT")]
|
||||||
pub(crate) default_post_sort: Option<String>,
|
pub(crate) default_post_sort: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_BLUR_SPOILER")]
|
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_BLUR_SPOILER")]
|
|
||||||
pub(crate) default_blur_spoiler: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
|
#[serde(rename = "REDLIB_DEFAULT_SHOW_NSFW")]
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_SHOW_NSFW")]
|
||||||
pub(crate) default_show_nsfw: Option<String>,
|
pub(crate) default_show_nsfw: Option<String>,
|
||||||
@ -68,10 +60,6 @@ pub struct Config {
|
|||||||
#[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_USE_HLS")]
|
||||||
pub(crate) default_use_hls: Option<String>,
|
pub(crate) default_use_hls: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
|
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_FFMPEG_VIDEO_DOWNLOADS")]
|
|
||||||
pub(crate) default_ffmpeg_video_downloads: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
#[serde(rename = "REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
|
||||||
pub(crate) default_hide_hls_notification: Option<String>,
|
pub(crate) default_hide_hls_notification: Option<String>,
|
||||||
@ -80,10 +68,6 @@ pub struct Config {
|
|||||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_AWARDS")]
|
||||||
pub(crate) default_hide_awards: Option<String>,
|
pub(crate) default_hide_awards: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
|
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY")]
|
|
||||||
pub(crate) default_hide_sidebar_and_summary: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")]
|
#[serde(rename = "REDLIB_DEFAULT_HIDE_SCORE")]
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_HIDE_SCORE")]
|
||||||
pub(crate) default_hide_score: Option<String>,
|
pub(crate) default_hide_score: Option<String>,
|
||||||
@ -92,10 +76,6 @@ pub struct Config {
|
|||||||
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_SUBSCRIPTIONS")]
|
||||||
pub(crate) default_subscriptions: Option<String>,
|
pub(crate) default_subscriptions: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_FILTERS")]
|
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
|
|
||||||
pub(crate) default_filters: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
||||||
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
|
||||||
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
|
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
|
||||||
@ -111,12 +91,6 @@ pub struct Config {
|
|||||||
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
|
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
|
||||||
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
|
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
|
||||||
pub(crate) pushshift: Option<String>,
|
pub(crate) pushshift: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_ENABLE_RSS")]
|
|
||||||
pub(crate) enable_rss: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "REDLIB_FULL_URL")]
|
|
||||||
pub(crate) full_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -129,7 +103,7 @@ impl Config {
|
|||||||
new_file.ok().and_then(|new_file| toml::from_str::<Self>(&new_file).ok())
|
new_file.ok().and_then(|new_file| toml::from_str::<Self>(&new_file).ok())
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = load_config("redlib.toml").or_else(|| load_config("libreddit.toml")).unwrap_or_default();
|
let config = load_config("redlib.toml").or(load_config("libreddit.toml")).unwrap_or_default();
|
||||||
|
|
||||||
// This function defines the order of preference - first check for
|
// This function defines the order of preference - first check for
|
||||||
// environment variables with "REDLIB", then check the legacy LIBREDDIT
|
// environment variables with "REDLIB", then check the legacy LIBREDDIT
|
||||||
@ -138,34 +112,27 @@ impl Config {
|
|||||||
// Return the first non-`None` value
|
// Return the first non-`None` value
|
||||||
// If all are `None`, return `None`
|
// If all are `None`, return `None`
|
||||||
let legacy_key = key.replace("REDLIB_", "LIBREDDIT_");
|
let legacy_key = key.replace("REDLIB_", "LIBREDDIT_");
|
||||||
var(key).ok().or_else(|| var(legacy_key).ok()).or_else(|| get_setting_from_config(key, &config))
|
var(key).ok().or(var(legacy_key).ok()).or(get_setting_from_config(key, &config))
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
sfw_only: parse("REDLIB_SFW_ONLY"),
|
sfw_only: parse("REDLIB_SFW_ONLY"),
|
||||||
default_theme: parse("REDLIB_DEFAULT_THEME"),
|
default_theme: parse("REDLIB_DEFAULT_THEME"),
|
||||||
default_mascot: parse("REDLIB_DEFAULT_MASCOT"),
|
|
||||||
default_front_page: parse("REDLIB_DEFAULT_FRONT_PAGE"),
|
default_front_page: parse("REDLIB_DEFAULT_FRONT_PAGE"),
|
||||||
default_layout: parse("REDLIB_DEFAULT_LAYOUT"),
|
default_layout: parse("REDLIB_DEFAULT_LAYOUT"),
|
||||||
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
|
default_post_sort: parse("REDLIB_DEFAULT_POST_SORT"),
|
||||||
default_wide: parse("REDLIB_DEFAULT_WIDE"),
|
default_wide: parse("REDLIB_DEFAULT_WIDE"),
|
||||||
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
|
default_comment_sort: parse("REDLIB_DEFAULT_COMMENT_SORT"),
|
||||||
default_blur_spoiler: parse("REDLIB_DEFAULT_BLUR_SPOILER"),
|
|
||||||
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
|
default_show_nsfw: parse("REDLIB_DEFAULT_SHOW_NSFW"),
|
||||||
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
|
default_blur_nsfw: parse("REDLIB_DEFAULT_BLUR_NSFW"),
|
||||||
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
|
default_use_hls: parse("REDLIB_DEFAULT_USE_HLS"),
|
||||||
default_ffmpeg_video_downloads: parse("REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS"),
|
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS"),
|
||||||
default_hide_hls_notification: parse("REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION"),
|
|
||||||
default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"),
|
default_hide_awards: parse("REDLIB_DEFAULT_HIDE_AWARDS"),
|
||||||
default_hide_sidebar_and_summary: parse("REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY"),
|
|
||||||
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
|
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
|
||||||
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
|
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
|
||||||
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
|
|
||||||
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
|
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
|
||||||
banner: parse("REDLIB_BANNER"),
|
banner: parse("REDLIB_BANNER"),
|
||||||
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
|
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
|
||||||
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
|
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
|
||||||
enable_rss: parse("REDLIB_ENABLE_RSS"),
|
|
||||||
full_url: parse("REDLIB_FULL_URL"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,29 +141,22 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
|
|||||||
match name {
|
match name {
|
||||||
"REDLIB_SFW_ONLY" => config.sfw_only.clone(),
|
"REDLIB_SFW_ONLY" => config.sfw_only.clone(),
|
||||||
"REDLIB_DEFAULT_THEME" => config.default_theme.clone(),
|
"REDLIB_DEFAULT_THEME" => config.default_theme.clone(),
|
||||||
"REDLIB_DEFAULT_MASCOT" => config.default_mascot.clone(),
|
|
||||||
"REDLIB_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(),
|
"REDLIB_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(),
|
||||||
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
|
"REDLIB_DEFAULT_LAYOUT" => config.default_layout.clone(),
|
||||||
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
|
"REDLIB_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
|
||||||
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
|
"REDLIB_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
|
||||||
"REDLIB_DEFAULT_BLUR_SPOILER" => config.default_blur_spoiler.clone(),
|
|
||||||
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
|
"REDLIB_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
|
||||||
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
|
"REDLIB_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
|
||||||
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
|
"REDLIB_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
|
||||||
"REDLIB_DEFAULT_FFMPEG_VIDEO_DOWNLOADS" => config.default_ffmpeg_video_downloads.clone(),
|
|
||||||
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
|
"REDLIB_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
|
||||||
"REDLIB_DEFAULT_WIDE" => config.default_wide.clone(),
|
"REDLIB_DEFAULT_WIDE" => config.default_wide.clone(),
|
||||||
"REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
|
"REDLIB_DEFAULT_HIDE_AWARDS" => config.default_hide_awards.clone(),
|
||||||
"REDLIB_DEFAULT_HIDE_SIDEBAR_AND_SUMMARY" => config.default_hide_sidebar_and_summary.clone(),
|
|
||||||
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
|
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
|
||||||
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
|
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
|
||||||
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
|
|
||||||
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
|
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
|
||||||
"REDLIB_BANNER" => config.banner.clone(),
|
"REDLIB_BANNER" => config.banner.clone(),
|
||||||
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
|
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
|
||||||
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
|
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
|
||||||
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
|
|
||||||
"REDLIB_FULL_URL" => config.full_url.clone(),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,12 +225,6 @@ fn test_default_subscriptions() {
|
|||||||
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
|
assert_eq!(get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"), Some("news+bestof".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[sealed_test(env = [("REDLIB_DEFAULT_FILTERS", "news+bestof")])]
|
|
||||||
fn test_default_filters() {
|
|
||||||
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[sealed_test]
|
#[sealed_test]
|
||||||
fn test_pushshift() {
|
fn test_pushshift() {
|
||||||
|
@ -5,21 +5,21 @@ use crate::server::RequestExt;
|
|||||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||||
use crate::utils::{error, filter_posts, get_filters, nsfw_landing, parse_post, 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};
|
use hyper::{Body, Request, Response};
|
||||||
use rinja::Template;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
|
||||||
/// `DuplicatesParams` contains the parameters in the URL.
|
/// DuplicatesParams contains the parameters in the URL.
|
||||||
struct DuplicatesParams {
|
struct DuplicatesParams {
|
||||||
before: String,
|
before: String,
|
||||||
after: String,
|
after: String,
|
||||||
sort: String,
|
sort: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `DuplicatesTemplate` defines an Askama template for rendering duplicate
|
/// DuplicatesTemplate defines an Askama template for rendering duplicate
|
||||||
/// posts.
|
/// posts.
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "duplicates.html")]
|
#[template(path = "duplicates.html")]
|
||||||
@ -59,7 +59,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
// Log the request in debugging mode
|
// Log the request in debugging mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
req.param("id").unwrap_or_default();
|
dbg!(req.param("id").unwrap_or_default());
|
||||||
|
|
||||||
// Send the GET, and await JSON.
|
// Send the GET, and await JSON.
|
||||||
match json(path, quarantined).await {
|
match json(path, quarantined).await {
|
||||||
@ -151,7 +151,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if have_after {
|
if have_after {
|
||||||
"t3_".clone_into(&mut before);
|
before = "t3_".to_owned();
|
||||||
before.push_str(&duplicates[0].id);
|
before.push_str(&duplicates[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
if have_before {
|
if have_before {
|
||||||
// The next batch will need to start from one after the
|
// The next batch will need to start from one after the
|
||||||
// last post in the current batch.
|
// last post in the current batch.
|
||||||
"t3_".clone_into(&mut after);
|
after = "t3_".to_owned();
|
||||||
after.push_str(&duplicates[l - 1].id);
|
after.push_str(&duplicates[l - 1].id);
|
||||||
|
|
||||||
// Here is where things get terrible. Notice that we
|
// Here is where things get terrible. Notice that we
|
||||||
@ -182,14 +182,14 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
match json(new_path, true).await {
|
match json(new_path, true).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() {
|
if !response[1]["data"]["children"].as_array().unwrap_or(&Vec::new()).is_empty() {
|
||||||
"t3_".clone_into(&mut before);
|
before = "t3_".to_owned();
|
||||||
before.push_str(&duplicates[0].id);
|
before.push_str(&duplicates[0].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
// Abort entirely if we couldn't get the previous
|
// Abort entirely if we couldn't get the previous
|
||||||
// batch.
|
// batch.
|
||||||
return error(req, &msg).await;
|
return error(req, msg).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -197,7 +197,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(template(&DuplicatesTemplate {
|
template(DuplicatesTemplate {
|
||||||
params: DuplicatesParams { before, after, sort },
|
params: DuplicatesParams { before, after, sort },
|
||||||
post,
|
post,
|
||||||
duplicates,
|
duplicates,
|
||||||
@ -205,28 +205,28 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
url: req_url,
|
url: req_url,
|
||||||
num_posts_filtered,
|
num_posts_filtered,
|
||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process error.
|
// Process error.
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" || msg == "gated" {
|
if msg == "quarantined" || msg == "gated" {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
Ok(quarantine(&req, sub, &msg))
|
quarantine(req, sub, msg)
|
||||||
} else {
|
} else {
|
||||||
error(req, &msg).await
|
error(req, msg).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DUPLICATES
|
// DUPLICATES
|
||||||
async fn parse_duplicates(json: &Value, filters: &HashSet<String>) -> (Vec<Post>, u64, bool) {
|
async fn parse_duplicates(json: &serde_json::Value, filters: &HashSet<String>) -> (Vec<Post>, u64, bool) {
|
||||||
let post_duplicates: &Vec<Value> = &json["data"]["children"].as_array().map_or(Vec::new(), ToOwned::to_owned);
|
let post_duplicates: &Vec<Value> = &json["data"]["children"].as_array().map_or(Vec::new(), ToOwned::to_owned);
|
||||||
let mut duplicates: Vec<Post> = Vec::new();
|
let mut duplicates: Vec<Post> = Vec::new();
|
||||||
|
|
||||||
// Process each post and place them in the Vec<Post>.
|
// Process each post and place them in the Vec<Post>.
|
||||||
for val in post_duplicates {
|
for val in post_duplicates.iter() {
|
||||||
let post: Post = parse_post(val).await;
|
let post: Post = parse_post(val).await;
|
||||||
duplicates.push(post);
|
duplicates.push(post);
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ use crate::{
|
|||||||
server::RequestExt,
|
server::RequestExt,
|
||||||
utils::{ErrorTemplate, Preferences},
|
utils::{ErrorTemplate, Preferences},
|
||||||
};
|
};
|
||||||
|
use askama::Template;
|
||||||
use build_html::{Container, Html, HtmlContainer, Table};
|
use build_html::{Container, Html, HtmlContainer, Table};
|
||||||
use hyper::{http::Error, Body, Request, Response};
|
use hyper::{http::Error, Body, Request, Response};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rinja::Template;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ pub async fn instance_info(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
"yaml" | "yml" => info_yaml(),
|
"yaml" | "yml" => info_yaml(),
|
||||||
"txt" => info_txt(),
|
"txt" => info_txt(),
|
||||||
"json" => info_json(),
|
"json" => info_json(),
|
||||||
"html" | "" => info_html(&req),
|
"html" | "" => info_html(req),
|
||||||
_ => {
|
_ => {
|
||||||
let error = ErrorTemplate {
|
let error = ErrorTemplate {
|
||||||
msg: "Error: Invalid info extension".into(),
|
msg: "Error: Invalid info extension".into(),
|
||||||
@ -68,13 +68,13 @@ fn info_txt() -> Result<Response<Body>, Error> {
|
|||||||
Response::builder()
|
Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("content-type", "text/plain")
|
.header("content-type", "text/plain")
|
||||||
.body(Body::from(INSTANCE_INFO.to_string(&StringType::Raw)))
|
.body(Body::from(INSTANCE_INFO.to_string(StringType::Raw)))
|
||||||
}
|
}
|
||||||
fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
|
fn info_html(req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||||
let message = MessageTemplate {
|
let message = MessageTemplate {
|
||||||
title: String::from("Instance information"),
|
title: String::from("Instance information"),
|
||||||
body: INSTANCE_INFO.to_string(&StringType::Html),
|
body: INSTANCE_INFO.to_string(StringType::Html),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(&req),
|
||||||
url: req.uri().to_string(),
|
url: req.uri().to_string(),
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
@ -85,7 +85,7 @@ fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
|
|||||||
pub struct InstanceInfo {
|
pub struct InstanceInfo {
|
||||||
package_name: String,
|
package_name: String,
|
||||||
crate_version: String,
|
crate_version: String,
|
||||||
pub git_commit: String,
|
git_commit: String,
|
||||||
deploy_date: String,
|
deploy_date: String,
|
||||||
compile_mode: String,
|
compile_mode: String,
|
||||||
deploy_unix_ts: i64,
|
deploy_unix_ts: i64,
|
||||||
@ -109,7 +109,7 @@ impl InstanceInfo {
|
|||||||
}
|
}
|
||||||
fn to_table(&self) -> String {
|
fn to_table(&self) -> String {
|
||||||
let mut container = Container::default();
|
let mut container = Container::default();
|
||||||
let convert = |o: &Option<String>| -> String { o.clone().unwrap_or_else(|| "<span class=\"unset\"><i>Unset</i></span>".to_owned()) };
|
let convert = |o: &Option<String>| -> String { o.clone().unwrap_or("<span class=\"unset\"><i>Unset</i></span>".to_owned()) };
|
||||||
if let Some(banner) = &self.config.banner {
|
if let Some(banner) = &self.config.banner {
|
||||||
container.add_header(3, "Instance banner");
|
container.add_header(3, "Instance banner");
|
||||||
container.add_raw("<br />");
|
container.add_raw("<br />");
|
||||||
@ -126,8 +126,6 @@ impl InstanceInfo {
|
|||||||
["Compile mode", &self.compile_mode],
|
["Compile mode", &self.compile_mode],
|
||||||
["SFW only", &convert(&self.config.sfw_only)],
|
["SFW only", &convert(&self.config.sfw_only)],
|
||||||
["Pushshift frontend", &convert(&self.config.pushshift)],
|
["Pushshift frontend", &convert(&self.config.pushshift)],
|
||||||
["RSS enabled", &convert(&self.config.enable_rss)],
|
|
||||||
["Full URL", &convert(&self.config.full_url)],
|
|
||||||
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
|
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
|
||||||
])
|
])
|
||||||
.with_header_row(["Settings"]),
|
.with_header_row(["Settings"]),
|
||||||
@ -138,26 +136,22 @@ impl InstanceInfo {
|
|||||||
["Hide awards", &convert(&self.config.default_hide_awards)],
|
["Hide awards", &convert(&self.config.default_hide_awards)],
|
||||||
["Hide score", &convert(&self.config.default_hide_score)],
|
["Hide score", &convert(&self.config.default_hide_score)],
|
||||||
["Theme", &convert(&self.config.default_theme)],
|
["Theme", &convert(&self.config.default_theme)],
|
||||||
["Mascot", &convert(&self.config.default_mascot)],
|
|
||||||
["Front page", &convert(&self.config.default_front_page)],
|
["Front page", &convert(&self.config.default_front_page)],
|
||||||
["Layout", &convert(&self.config.default_layout)],
|
["Layout", &convert(&self.config.default_layout)],
|
||||||
["Wide", &convert(&self.config.default_wide)],
|
["Wide", &convert(&self.config.default_wide)],
|
||||||
["Comment sort", &convert(&self.config.default_comment_sort)],
|
["Comment sort", &convert(&self.config.default_comment_sort)],
|
||||||
["Post sort", &convert(&self.config.default_post_sort)],
|
["Post sort", &convert(&self.config.default_post_sort)],
|
||||||
["Blur Spoiler", &convert(&self.config.default_blur_spoiler)],
|
|
||||||
["Show NSFW", &convert(&self.config.default_show_nsfw)],
|
["Show NSFW", &convert(&self.config.default_show_nsfw)],
|
||||||
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
|
["Blur NSFW", &convert(&self.config.default_blur_nsfw)],
|
||||||
["Use HLS", &convert(&self.config.default_use_hls)],
|
["Use HLS", &convert(&self.config.default_use_hls)],
|
||||||
["Use FFmpeg", &convert(&self.config.default_ffmpeg_video_downloads)],
|
|
||||||
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
|
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
|
||||||
["Subscriptions", &convert(&self.config.default_subscriptions)],
|
["Subscriptions", &convert(&self.config.default_subscriptions)],
|
||||||
["Filters", &convert(&self.config.default_filters)],
|
|
||||||
])
|
])
|
||||||
.with_header_row(["Default preferences"]),
|
.with_header_row(["Default preferences"]),
|
||||||
);
|
);
|
||||||
container.to_html_string().replace("<th>", "<th colspan=\"2\">")
|
container.to_html_string().replace("<th>", "<th colspan=\"2\">")
|
||||||
}
|
}
|
||||||
fn to_string(&self, string_type: &StringType) -> String {
|
fn to_string(&self, string_type: StringType) -> String {
|
||||||
match string_type {
|
match string_type {
|
||||||
StringType::Raw => {
|
StringType::Raw => {
|
||||||
format!(
|
format!(
|
||||||
@ -169,27 +163,21 @@ impl InstanceInfo {
|
|||||||
Compile mode: {}\n
|
Compile mode: {}\n
|
||||||
SFW only: {:?}\n
|
SFW only: {:?}\n
|
||||||
Pushshift frontend: {:?}\n
|
Pushshift frontend: {:?}\n
|
||||||
RSS enabled: {:?}\n
|
|
||||||
Full URL: {:?}\n
|
|
||||||
Config:\n
|
Config:\n
|
||||||
Banner: {:?}\n
|
Banner: {:?}\n
|
||||||
Hide awards: {:?}\n
|
Hide awards: {:?}\n
|
||||||
Hide score: {:?}\n
|
Hide score: {:?}\n
|
||||||
Default theme: {:?}\n
|
Default theme: {:?}\n
|
||||||
Default mascot: {:?}\n
|
|
||||||
Default front page: {:?}\n
|
Default front page: {:?}\n
|
||||||
Default layout: {:?}\n
|
Default layout: {:?}\n
|
||||||
Default wide: {:?}\n
|
Default wide: {:?}\n
|
||||||
Default comment sort: {:?}\n
|
Default comment sort: {:?}\n
|
||||||
Default post sort: {:?}\n
|
Default post sort: {:?}\n
|
||||||
Default blur Spoiler: {:?}\n
|
|
||||||
Default show NSFW: {:?}\n
|
Default show NSFW: {:?}\n
|
||||||
Default blur NSFW: {:?}\n
|
Default blur NSFW: {:?}\n
|
||||||
Default use HLS: {:?}\n
|
Default use HLS: {:?}\n
|
||||||
Default use FFmpeg: {:?}\n
|
|
||||||
Default hide HLS notification: {:?}\n
|
Default hide HLS notification: {:?}\n
|
||||||
Default subscriptions: {:?}\n
|
Default subscriptions: {:?}\n",
|
||||||
Default filters: {:?}\n",
|
|
||||||
self.package_name,
|
self.package_name,
|
||||||
self.crate_version,
|
self.crate_version,
|
||||||
self.git_commit,
|
self.git_commit,
|
||||||
@ -197,27 +185,21 @@ impl InstanceInfo {
|
|||||||
self.deploy_unix_ts,
|
self.deploy_unix_ts,
|
||||||
self.compile_mode,
|
self.compile_mode,
|
||||||
self.config.sfw_only,
|
self.config.sfw_only,
|
||||||
self.config.enable_rss,
|
|
||||||
self.config.full_url,
|
|
||||||
self.config.pushshift,
|
self.config.pushshift,
|
||||||
self.config.banner,
|
self.config.banner,
|
||||||
self.config.default_hide_awards,
|
self.config.default_hide_awards,
|
||||||
self.config.default_hide_score,
|
self.config.default_hide_score,
|
||||||
self.config.default_theme,
|
self.config.default_theme,
|
||||||
self.config.default_mascot,
|
|
||||||
self.config.default_front_page,
|
self.config.default_front_page,
|
||||||
self.config.default_layout,
|
self.config.default_layout,
|
||||||
self.config.default_wide,
|
self.config.default_wide,
|
||||||
self.config.default_comment_sort,
|
self.config.default_comment_sort,
|
||||||
self.config.default_post_sort,
|
self.config.default_post_sort,
|
||||||
self.config.default_blur_spoiler,
|
|
||||||
self.config.default_show_nsfw,
|
self.config.default_show_nsfw,
|
||||||
self.config.default_blur_nsfw,
|
self.config.default_blur_nsfw,
|
||||||
self.config.default_use_hls,
|
self.config.default_use_hls,
|
||||||
self.config.default_ffmpeg_video_downloads,
|
|
||||||
self.config.default_hide_hls_notification,
|
self.config.default_hide_hls_notification,
|
||||||
self.config.default_subscriptions,
|
self.config.default_subscriptions,
|
||||||
self.config.default_filters,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
StringType::Html => self.to_table(),
|
StringType::Html => self.to_table(),
|
||||||
|
13
src/lib.rs
@ -1,13 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod config;
|
|
||||||
pub mod duplicates;
|
|
||||||
pub mod instance_info;
|
|
||||||
pub mod oauth;
|
|
||||||
pub mod oauth_resources;
|
|
||||||
pub mod post;
|
|
||||||
pub mod search;
|
|
||||||
pub mod server;
|
|
||||||
pub mod settings;
|
|
||||||
pub mod subreddit;
|
|
||||||
pub mod user;
|
|
||||||
pub mod utils;
|
|
182
src/main.rs
@ -2,21 +2,35 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![allow(clippy::cmp_owned)]
|
#![allow(clippy::cmp_owned)]
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
// Reference local files
|
||||||
|
mod config;
|
||||||
|
mod duplicates;
|
||||||
|
mod instance_info;
|
||||||
|
mod oauth;
|
||||||
|
mod oauth_resources;
|
||||||
|
mod post;
|
||||||
|
mod search;
|
||||||
|
mod settings;
|
||||||
|
mod subreddit;
|
||||||
|
mod user;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
// Import Crates
|
||||||
use clap::{Arg, ArgAction, Command};
|
use clap::{Arg, ArgAction, Command};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use futures_lite::FutureExt;
|
use futures_lite::FutureExt;
|
||||||
use hyper::Uri;
|
|
||||||
use hyper::{header::HeaderValue, Body, Request, Response};
|
use hyper::{header::HeaderValue, Body, Request, Response};
|
||||||
|
|
||||||
|
mod client;
|
||||||
|
use client::{canonical_path, proxy};
|
||||||
use log::info;
|
use log::info;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use redsunlib::client::{canonical_path, proxy, CLIENT};
|
use server::RequestExt;
|
||||||
use redsunlib::server::{self, RequestExt};
|
use utils::{error, redirect, ThemeAssets};
|
||||||
use redsunlib::utils::{error, redirect, ThemeAssets, MascotAssets};
|
|
||||||
use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user};
|
|
||||||
|
|
||||||
use redsunlib::client::OAUTH_CLIENT;
|
use crate::client::OAUTH_CLIENT;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
|
||||||
// Create Services
|
// Create Services
|
||||||
|
|
||||||
@ -64,17 +78,6 @@ async fn font() -> Result<Response<Body>, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ffmpeg() -> Result<Response<Body>, String> {
|
|
||||||
Ok(
|
|
||||||
Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header("content-type", "application/wasm")
|
|
||||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
|
||||||
.body(include_bytes!("../static/ffmpeg/ffmpeg-core.wasm").as_ref().into())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
|
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
|
||||||
let mut res = Response::builder()
|
let mut res = Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
@ -108,20 +111,6 @@ async fn style() -> Result<Response<Body>, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve mascot
|
|
||||||
async fn mascot_image(req: Request<Body>) -> Result<Response<Body>, String> {
|
|
||||||
let res = MascotAssets::get(&req.param("name").unwrap())
|
|
||||||
.unwrap_or(MascotAssets::get("redsunlib.png").unwrap());
|
|
||||||
Ok(
|
|
||||||
Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header("content-type", "image/png")
|
|
||||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
|
||||||
.body(res.data.into())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
@ -146,7 +135,7 @@ async fn main() {
|
|||||||
.long("address")
|
.long("address")
|
||||||
.value_name("ADDRESS")
|
.value_name("ADDRESS")
|
||||||
.help("Sets address to listen on")
|
.help("Sets address to listen on")
|
||||||
.default_value("[::]")
|
.default_value("0.0.0.0")
|
||||||
.num_args(1),
|
.num_args(1),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@ -177,7 +166,7 @@ async fn main() {
|
|||||||
|
|
||||||
let listener = [address, ":", port].concat();
|
let listener = [address, ":", port].concat();
|
||||||
|
|
||||||
println!("Starting Redsunlib...");
|
println!("Starting Redlib...");
|
||||||
|
|
||||||
// Begin constructing a server
|
// Begin constructing a server
|
||||||
let mut app = server::Server::new();
|
let mut app = server::Server::new();
|
||||||
@ -200,11 +189,11 @@ async fn main() {
|
|||||||
"Referrer-Policy" => "no-referrer",
|
"Referrer-Policy" => "no-referrer",
|
||||||
"X-Content-Type-Options" => "nosniff",
|
"X-Content-Type-Options" => "nosniff",
|
||||||
"X-Frame-Options" => "DENY",
|
"X-Frame-Options" => "DENY",
|
||||||
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src 'self' blob:;"
|
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;"
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(expire_time) = hsts {
|
if let Some(expire_time) = hsts {
|
||||||
if let Ok(val) = HeaderValue::from_str(&format!("max-age={expire_time}")) {
|
if let Ok(val) = HeaderValue::from_str(&format!("max-age={}", expire_time)) {
|
||||||
app.default_headers.insert("Strict-Transport-Security", val);
|
app.default_headers.insert("Strict-Transport-Security", val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,41 +224,14 @@ async fn main() {
|
|||||||
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
||||||
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
||||||
app
|
app
|
||||||
.at("/videoUtils.js")
|
.at("/playHLSVideo.js")
|
||||||
.get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed());
|
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
|
||||||
app
|
app
|
||||||
.at("/hls.min.js")
|
.at("/hls.min.js")
|
||||||
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
|
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
|
||||||
app
|
app
|
||||||
.at("/highlighted.js")
|
.at("/highlighted.js")
|
||||||
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
|
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
|
||||||
app
|
|
||||||
.at("/check_update.js")
|
|
||||||
.get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed());
|
|
||||||
|
|
||||||
app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed());
|
|
||||||
app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed());
|
|
||||||
|
|
||||||
// FFmpeg
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/814.ffmpeg.js")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js"), "text/javascript", false).boxed());
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/814.ffmpeg.js.map")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/814.ffmpeg.js.map"), "text/javascript", false).boxed());
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/ffmpeg-core.js")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-core.js"), "text/javascript", false).boxed());
|
|
||||||
app.at("/ffmpeg/ffmpeg-core.wasm").get(|_| ffmpeg().boxed());
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/ffmpeg-util.js")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg-util.js"), "text/javascript", false).boxed());
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/ffmpeg.js")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js"), "text/javascript", false).boxed());
|
|
||||||
app
|
|
||||||
.at("/ffmpeg/ffmpeg.js.map")
|
|
||||||
.get(|_| resource(include_str!("../static/ffmpeg/ffmpeg.js.map"), "text/javascript", false).boxed());
|
|
||||||
|
|
||||||
// Proxy media through Redlib
|
// Proxy media through Redlib
|
||||||
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
|
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
|
||||||
@ -277,9 +239,6 @@ async fn main() {
|
|||||||
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
||||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||||
app
|
|
||||||
.at("/emote/:subreddit_id/:filename")
|
|
||||||
.get(|r| proxy(r, "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/{subreddit_id}/{filename}").boxed());
|
|
||||||
app
|
app
|
||||||
.at("/preview/:loc/award_images/:fullname/:id")
|
.at("/preview/:loc/award_images/:fullname/:id")
|
||||||
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
|
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
|
||||||
@ -290,12 +249,11 @@ async fn main() {
|
|||||||
// Browse user profile
|
// Browse user profile
|
||||||
app
|
app
|
||||||
.at("/u/:name")
|
.at("/u/:name")
|
||||||
.get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
||||||
app.at("/u/:name/comments/:id/:title").get(|r| post::item(r).boxed());
|
app.at("/u/:name/comments/:id/:title").get(|r| post::item(r).boxed());
|
||||||
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
||||||
|
|
||||||
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
|
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed());
|
||||||
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
|
|
||||||
app.at("/user/:name").get(|r| user::profile(r).boxed());
|
app.at("/user/:name").get(|r| user::profile(r).boxed());
|
||||||
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
|
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
|
||||||
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
|
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
|
||||||
@ -307,12 +265,6 @@ async fn main() {
|
|||||||
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
|
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
|
||||||
app.at("/settings/update").get(|r| settings::update(r).boxed());
|
app.at("/settings/update").get(|r| settings::update(r).boxed());
|
||||||
|
|
||||||
// Mascots
|
|
||||||
app.at("/mascot/:name").get(|r| mascot_image(r).boxed());
|
|
||||||
|
|
||||||
// RSS Subscriptions
|
|
||||||
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());
|
|
||||||
|
|
||||||
// Subreddit services
|
// Subreddit services
|
||||||
app
|
app
|
||||||
.at("/r/:sub")
|
.at("/r/:sub")
|
||||||
@ -321,7 +273,7 @@ async fn main() {
|
|||||||
|
|
||||||
app
|
app
|
||||||
.at("/r/u_:name")
|
.at("/r/u_:name")
|
||||||
.get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
||||||
|
|
||||||
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
@ -346,10 +298,10 @@ async fn main() {
|
|||||||
|
|
||||||
app
|
app
|
||||||
.at("/r/:sub/w")
|
.at("/r/:sub/w")
|
||||||
.get(|r| async move { Ok(redirect(&format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed());
|
||||||
app
|
app
|
||||||
.at("/r/:sub/w/*page")
|
.at("/r/:sub/w/*page")
|
||||||
.get(|r| async move { Ok(redirect(&format!("/r/{}/wiki/{}", r.param("sub").unwrap_or_default(), r.param("wiki").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki/{}", r.param("sub").unwrap_or_default(), r.param("wiki").unwrap_or_default()))) }.boxed());
|
||||||
app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed());
|
app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed());
|
||||||
app.at("/r/:sub/wiki/*page").get(|r| subreddit::wiki(r).boxed());
|
app.at("/r/:sub/wiki/*page").get(|r| subreddit::wiki(r).boxed());
|
||||||
|
|
||||||
@ -361,10 +313,10 @@ async fn main() {
|
|||||||
app.at("/").get(|r| subreddit::community(r).boxed());
|
app.at("/").get(|r| subreddit::community(r).boxed());
|
||||||
|
|
||||||
// View Reddit wiki
|
// View Reddit wiki
|
||||||
app.at("/w").get(|_| async { Ok(redirect("/wiki")) }.boxed());
|
app.at("/w").get(|_| async { Ok(redirect("/wiki".to_string())) }.boxed());
|
||||||
app
|
app
|
||||||
.at("/w/*page")
|
.at("/w/*page")
|
||||||
.get(|r| async move { Ok(redirect(&format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed());
|
||||||
app.at("/wiki").get(|r| subreddit::wiki(r).boxed());
|
app.at("/wiki").get(|r| subreddit::wiki(r).boxed());
|
||||||
app.at("/wiki/*page").get(|r| subreddit::wiki(r).boxed());
|
app.at("/wiki/*page").get(|r| subreddit::wiki(r).boxed());
|
||||||
|
|
||||||
@ -372,7 +324,7 @@ async fn main() {
|
|||||||
app.at("/search").get(|r| search::find(r).boxed());
|
app.at("/search").get(|r| search::find(r).boxed());
|
||||||
|
|
||||||
// Handle about pages
|
// Handle about pages
|
||||||
app.at("/about").get(|req| error(req, "About pages aren't added yet").boxed());
|
app.at("/about").get(|req| error(req, "About pages aren't added yet".to_string()).boxed());
|
||||||
|
|
||||||
// Instance info page
|
// Instance info page
|
||||||
app.at("/info").get(|r| instance_info::instance_info(r).boxed());
|
app.at("/info").get(|r| instance_info::instance_info(r).boxed());
|
||||||
@ -385,14 +337,14 @@ async fn main() {
|
|||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
match req.param("id").as_deref() {
|
match req.param("id").as_deref() {
|
||||||
// Share link
|
// Share link
|
||||||
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await {
|
Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{}/s/{}", sub, id)).await {
|
||||||
Ok(Some(path)) => Ok(redirect(&path)),
|
Ok(Some(path)) => Ok(redirect(path)),
|
||||||
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||||
Err(e) => error(req, &e).await,
|
Err(e) => error(req, e).await,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Error message for unknown pages
|
// Error message for unknown pages
|
||||||
_ => error(req, "Nothing here").await,
|
_ => error(req, "Nothing here".to_string()).await,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -404,67 +356,29 @@ async fn main() {
|
|||||||
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
|
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
|
||||||
|
|
||||||
// Short link for post
|
// Short link for post
|
||||||
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await {
|
Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{}", id)).await {
|
||||||
Ok(path_opt) => match path_opt {
|
Ok(path_opt) => match path_opt {
|
||||||
Some(path) => Ok(redirect(&path)),
|
Some(path) => Ok(redirect(path)),
|
||||||
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,
|
||||||
},
|
},
|
||||||
Err(e) => error(req, &e).await,
|
Err(e) => error(req, e).await,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Error message for unknown pages
|
// Error message for unknown pages
|
||||||
_ => error(req, "Nothing here").await,
|
_ => error(req, "Nothing here".to_string()).await,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default service in case no routes match
|
// Default service in case no routes match
|
||||||
app.at("/*").get(|req| error(req, "Nothing here").boxed());
|
app.at("/*").get(|req| error(req, "Nothing here".to_string()).boxed());
|
||||||
|
|
||||||
println!("Running Redsunlib v{} on {listener}!", env!("CARGO_PKG_VERSION"));
|
println!("Running Redlib v{} on {}!", env!("CARGO_PKG_VERSION"), listener);
|
||||||
|
|
||||||
let server = app.listen(&listener);
|
let server = app.listen(listener);
|
||||||
|
|
||||||
// Run this server for... forever!
|
// Run this server for... forever!
|
||||||
if let Err(e) = server.await {
|
if let Err(e) = server.await {
|
||||||
eprintln!("Server error: {e}");
|
eprintln!("Server error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn proxy_commit_info() -> Result<Response<Body>, String> {
|
|
||||||
Ok(
|
|
||||||
Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header("content-type", "application/atom+xml")
|
|
||||||
.body(Body::from(fetch_commit_info().await))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached(time = 600)]
|
|
||||||
async fn fetch_commit_info() -> String {
|
|
||||||
let uri = Uri::from_str("https://git.stardust.wtf/api/v1/repos/iridium/redsunlib/commits?verification=false&stat=false").expect("Invalid URI");
|
|
||||||
|
|
||||||
let resp: Body = CLIENT.get(uri).await.expect("Failed to request git.stardust.wtf").into_body();
|
|
||||||
|
|
||||||
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn proxy_instances() -> Result<Response<Body>, String> {
|
|
||||||
Ok(
|
|
||||||
Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(fetch_instances().await))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached(time = 600)]
|
|
||||||
async fn fetch_instances() -> String {
|
|
||||||
let uri = Uri::from_str("https://raw.githubusercontent.com/redlib-org/redlib-instances/refs/heads/main/instances.json").expect("Invalid URI");
|
|
||||||
|
|
||||||
let resp: Body = CLIENT.get(uri).await.expect("Failed to request GitHub").into_body();
|
|
||||||
|
|
||||||
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
|
|
||||||
}
|
|
||||||
|
84
src/oauth.rs
@ -1,19 +1,18 @@
|
|||||||
use std::{collections::HashMap, sync::atomic::Ordering, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{CLIENT, OAUTH_CLIENT, OAUTH_IS_ROLLING_OVER, OAUTH_RATELIMIT_REMAINING},
|
client::{CLIENT, OAUTH_CLIENT},
|
||||||
oauth_resources::ANDROID_APP_VERSION_LIST,
|
oauth_resources::ANDROID_APP_VERSION_LIST,
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use hyper::{client, Body, Method, Request};
|
use hyper::{client, Body, Method, Request};
|
||||||
use log::{error, info, trace};
|
use log::info;
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::time::{error::Elapsed, timeout};
|
|
||||||
|
|
||||||
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
|
static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg";
|
||||||
|
|
||||||
static AUTH_ENDPOINT: &str = "https://www.reddit.com";
|
static AUTH_ENDPOINT: &str = "https://accounts.reddit.com";
|
||||||
|
|
||||||
// Spoofed client for Android devices
|
// Spoofed client for Android devices
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@ -26,32 +25,11 @@ pub struct Oauth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Oauth {
|
impl Oauth {
|
||||||
/// Create a new OAuth client
|
|
||||||
pub(crate) async fn new() -> Self {
|
pub(crate) async fn new() -> Self {
|
||||||
// Call new_internal until it succeeds
|
|
||||||
loop {
|
|
||||||
let attempt = Self::new_with_timeout().await;
|
|
||||||
match attempt {
|
|
||||||
Ok(Some(oauth)) => {
|
|
||||||
info!("[✅] Successfully created OAuth client");
|
|
||||||
return oauth;
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
error!("Failed to create OAuth client. Retrying in 5 seconds...");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(duration) => {
|
|
||||||
error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new_with_timeout() -> Result<Option<Self>, Elapsed> {
|
|
||||||
let mut oauth = Self::default();
|
let mut oauth = Self::default();
|
||||||
timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth))
|
oauth.login().await;
|
||||||
|
oauth
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default() -> Self {
|
pub(crate) fn default() -> Self {
|
||||||
// Generate a device to spoof
|
// Generate a device to spoof
|
||||||
let device = Device::new();
|
let device = Device::new();
|
||||||
@ -68,11 +46,11 @@ impl Oauth {
|
|||||||
}
|
}
|
||||||
async fn login(&mut self) -> Option<()> {
|
async fn login(&mut self) -> Option<()> {
|
||||||
// Construct URL for OAuth token
|
// Construct URL for OAuth token
|
||||||
let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid");
|
let url = format!("{}/api/access_token", AUTH_ENDPOINT);
|
||||||
let mut builder = Request::builder().method(Method::POST).uri(&url);
|
let mut builder = Request::builder().method(Method::POST).uri(&url);
|
||||||
|
|
||||||
// Add headers from spoofed client
|
// Add headers from spoofed client
|
||||||
for (key, value) in &self.initial_headers {
|
for (key, value) in self.initial_headers.iter() {
|
||||||
builder = builder.header(key, value);
|
builder = builder.header(key, value);
|
||||||
}
|
}
|
||||||
// Set up HTTP Basic Auth - basically just the const OAuth ID's with no password,
|
// Set up HTTP Basic Auth - basically just the const OAuth ID's with no password,
|
||||||
@ -91,19 +69,13 @@ impl Oauth {
|
|||||||
// Build request
|
// Build request
|
||||||
let request = builder.body(body).unwrap();
|
let request = builder.body(body).unwrap();
|
||||||
|
|
||||||
trace!("Sending token request...");
|
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
let client: &once_cell::sync::Lazy<client::Client<_, Body>> = &CLIENT;
|
let client: client::Client<_, hyper::Body> = CLIENT.clone();
|
||||||
let resp = client.request(request).await.ok()?;
|
let resp = client.request(request).await.ok()?;
|
||||||
|
|
||||||
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));
|
|
||||||
|
|
||||||
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
|
// Parse headers - loid header _should_ be saved sent on subsequent token refreshes.
|
||||||
// Technically it's not needed, but it's easy for Reddit API to check for this.
|
// Technically it's not needed, but it's easy for Reddit API to check for this.
|
||||||
// It's some kind of header that uniquely identifies the device.
|
// It's some kind of header that uniquely identifies the device.
|
||||||
// Not worried about the privacy implications, since this is randomly changed
|
|
||||||
// and really only as privacy-concerning as the OAuth token itself.
|
|
||||||
if let Some(header) = resp.headers().get("x-reddit-loid") {
|
if let Some(header) = resp.headers().get("x-reddit-loid") {
|
||||||
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
|
self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string());
|
||||||
}
|
}
|
||||||
@ -113,14 +85,10 @@ impl Oauth {
|
|||||||
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
|
self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Serializing response...");
|
|
||||||
|
|
||||||
// Serialize response
|
// Serialize response
|
||||||
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
|
let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?;
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
|
let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?;
|
||||||
|
|
||||||
trace!("Accessing relevant fields...");
|
|
||||||
|
|
||||||
// Save token and expiry
|
// Save token and expiry
|
||||||
self.token = json.get("access_token")?.as_str()?.to_string();
|
self.token = json.get("access_token")?.as_str()?.to_string();
|
||||||
self.expires_in = json.get("expires_in")?.as_u64()?;
|
self.expires_in = json.get("expires_in")?.as_u64()?;
|
||||||
@ -130,13 +98,21 @@ impl Oauth {
|
|||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn refresh(&mut self) -> Option<()> {
|
||||||
|
// Refresh is actually just a subsequent login with the same headers (without the old token
|
||||||
|
// or anything). This logic is handled in login, so we just call login again.
|
||||||
|
let refresh = self.login().await;
|
||||||
|
info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" });
|
||||||
|
refresh
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn token_daemon() {
|
pub async fn token_daemon() {
|
||||||
// Monitor for refreshing token
|
// Monitor for refreshing token
|
||||||
loop {
|
loop {
|
||||||
// Get expiry time - be sure to not hold the read lock
|
// Get expiry time - be sure to not hold the read lock
|
||||||
let expires_in = { OAUTH_CLIENT.load_full().expires_in };
|
let expires_in = { OAUTH_CLIENT.read().await.expires_in };
|
||||||
|
|
||||||
// sleep for the expiry time minus 2 minutes
|
// sleep for the expiry time minus 2 minutes
|
||||||
let duration = Duration::from_secs(expires_in - 120);
|
let duration = Duration::from_secs(expires_in - 120);
|
||||||
@ -149,24 +125,10 @@ pub async fn token_daemon() {
|
|||||||
|
|
||||||
// Refresh token - in its own scope
|
// Refresh token - in its own scope
|
||||||
{
|
{
|
||||||
force_refresh_token().await;
|
OAUTH_CLIENT.write().await.refresh().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn force_refresh_token() {
|
|
||||||
if OAUTH_IS_ROLLING_OVER.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
|
|
||||||
trace!("Skipping refresh token roll over, already in progress");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("Rolling over refresh token. Current rate limit: {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst));
|
|
||||||
let new_client = Oauth::new().await;
|
|
||||||
OAUTH_CLIENT.swap(new_client.into());
|
|
||||||
OAUTH_RATELIMIT_REMAINING.store(99, Ordering::SeqCst);
|
|
||||||
OAUTH_IS_ROLLING_OVER.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct Device {
|
struct Device {
|
||||||
oauth_id: String,
|
oauth_id: String,
|
||||||
@ -212,21 +174,21 @@ fn choose<T: Copy>(list: &[T]) -> T {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_oauth_client() {
|
async fn test_oauth_client() {
|
||||||
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
|
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_oauth_client_refresh() {
|
async fn test_oauth_client_refresh() {
|
||||||
force_refresh_token().await;
|
OAUTH_CLIENT.write().await.refresh().await.unwrap();
|
||||||
}
|
}
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_oauth_token_exists() {
|
async fn test_oauth_token_exists() {
|
||||||
assert!(!OAUTH_CLIENT.load_full().token.is_empty());
|
assert!(!OAUTH_CLIENT.read().await.token.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_oauth_headers_len() {
|
async fn test_oauth_headers_len() {
|
||||||
assert!(OAUTH_CLIENT.load_full().headers_map.len() >= 3);
|
assert!(OAUTH_CLIENT.read().await.headers_map.len() >= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -2,43 +2,76 @@
|
|||||||
// Rerun scripts/update_oauth_resources.sh to update this file
|
// Rerun scripts/update_oauth_resources.sh to update this file
|
||||||
// Please do not edit manually
|
// Please do not edit manually
|
||||||
// Filled in with real app versions
|
// Filled in with real app versions
|
||||||
pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""];
|
pub static _IOS_APP_VERSION_LIST: &[&str; 67] = &[
|
||||||
|
"Version 2020.0.0/Build 306960",
|
||||||
|
"Version 2020.10.0/Build 307041",
|
||||||
|
"Version 2020.10.1/Build 307047",
|
||||||
|
"Version 2020.1.0/Build 306966",
|
||||||
|
"Version 2020.11.0/Build 307049",
|
||||||
|
"Version 2020.11.1/Build 307063",
|
||||||
|
"Version 2020.12.0/Build 307070",
|
||||||
|
"Version 2020.13.0/Build 307072",
|
||||||
|
"Version 2020.13.1/Build 307075",
|
||||||
|
"Version 2020.14.0/Build 307077",
|
||||||
|
"Version 2020.14.1/Build 307080",
|
||||||
|
"Version 2020.15.0/Build 307084",
|
||||||
|
"Version 2020.16.0/Build 307090",
|
||||||
|
"Version 2020.17.0/Build 307093",
|
||||||
|
"Version 2020.19.0/Build 307137",
|
||||||
|
"Version 2020.20.0/Build 307156",
|
||||||
|
"Version 2020.20.1/Build 307159",
|
||||||
|
"Version 2020.2.0/Build 306969",
|
||||||
|
"Version 2020.21.0/Build 307162",
|
||||||
|
"Version 2020.21.1/Build 307165",
|
||||||
|
"Version 2020.22.0/Build 307177",
|
||||||
|
"Version 2020.22.1/Build 307181",
|
||||||
|
"Version 2020.23.0/Build 307183",
|
||||||
|
"Version 2020.24.0/Build 307189",
|
||||||
|
"Version 2020.25.0/Build 307198",
|
||||||
|
"Version 2020.26.0/Build 307205",
|
||||||
|
"Version 2020.26.1/Build 307213",
|
||||||
|
"Version 2020.27.0/Build 307229",
|
||||||
|
"Version 2020.28.0/Build 307233",
|
||||||
|
"Version 2020.29.0/Build 307235",
|
||||||
|
"Version 2020.30.0/Build 307238",
|
||||||
|
"Version 2020.3.0/Build 306971",
|
||||||
|
"Version 2020.31.0/Build 307240",
|
||||||
|
"Version 2020.31.1/Build 307246",
|
||||||
|
"Version 2020.32.0/Build 307250",
|
||||||
|
"Version 2020.33.0/Build 307252",
|
||||||
|
"Version 2020.34.0/Build 307260",
|
||||||
|
"Version 2020.35.0/Build 307262",
|
||||||
|
"Version 2020.36.0/Build 307265",
|
||||||
|
"Version 2020.37.0/Build 307272",
|
||||||
|
"Version 2020.38.0/Build 307286",
|
||||||
|
"Version 2020.39.0/Build 307306",
|
||||||
|
"Version 2020.4.0/Build 306978",
|
||||||
|
"Version 2020.5.0/Build 306993",
|
||||||
|
"Version 2020.5.1/Build 307005",
|
||||||
|
"Version 2020.6.0/Build 307007",
|
||||||
|
"Version 2020.7.0/Build 307012",
|
||||||
|
"Version 2020.8.0/Build 307014",
|
||||||
|
"Version 2020.8.1/Build 307017",
|
||||||
|
"Version 2020.9.0/Build 307035",
|
||||||
|
"Version 2020.9.1/Build 307039",
|
||||||
|
"Version 2023.18.0/Build 310494",
|
||||||
|
"Version 2023.19.0/Build 310507",
|
||||||
|
"Version 2023.20.0/Build 310535",
|
||||||
|
"Version 2023.21.0/Build 310560",
|
||||||
|
"Version 2023.22.0/Build 613580",
|
||||||
|
"Version 2023.23.0/Build 310613",
|
||||||
|
"Version 2023.23.1/Build 613639",
|
||||||
|
"Version 2023.24.0/Build 613663",
|
||||||
|
"Version 2023.25.0/Build 613739",
|
||||||
|
"Version 2023.26.0/Build 613749",
|
||||||
|
"Version 2023.27.0/Build 613771",
|
||||||
|
"Version 2023.28.0/Build 613803",
|
||||||
|
"Version 2023.28.1/Build 613809",
|
||||||
|
"Version 2023.29.0/Build 613825",
|
||||||
|
"Version 2023.30.0/Build 613849",
|
||||||
|
"Version 2023.31.0/Build 613864",
|
||||||
|
];
|
||||||
pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
||||||
"Version 2023.48.0/Build 1319123",
|
|
||||||
"Version 2023.49.0/Build 1321715",
|
|
||||||
"Version 2023.49.1/Build 1322281",
|
|
||||||
"Version 2023.50.0/Build 1332338",
|
|
||||||
"Version 2023.50.1/Build 1345844",
|
|
||||||
"Version 2024.02.0/Build 1368985",
|
|
||||||
"Version 2024.03.0/Build 1379408",
|
|
||||||
"Version 2024.04.0/Build 1391236",
|
|
||||||
"Version 2024.05.0/Build 1403584",
|
|
||||||
"Version 2024.06.0/Build 1418489",
|
|
||||||
"Version 2024.07.0/Build 1429651",
|
|
||||||
"Version 2024.08.0/Build 1439531",
|
|
||||||
"Version 2024.10.0/Build 1470045",
|
|
||||||
"Version 2024.10.1/Build 1478645",
|
|
||||||
"Version 2024.11.0/Build 1480707",
|
|
||||||
"Version 2024.12.0/Build 1494694",
|
|
||||||
"Version 2024.13.0/Build 1505187",
|
|
||||||
"Version 2024.14.0/Build 1520556",
|
|
||||||
"Version 2024.15.0/Build 1536823",
|
|
||||||
"Version 2024.16.0/Build 1551366",
|
|
||||||
"Version 2024.17.0/Build 1568106",
|
|
||||||
"Version 2024.18.0/Build 1577901",
|
|
||||||
"Version 2024.18.1/Build 1585304",
|
|
||||||
"Version 2024.19.0/Build 1593346",
|
|
||||||
"Version 2024.20.0/Build 1612800",
|
|
||||||
"Version 2024.20.1/Build 1615586",
|
|
||||||
"Version 2024.20.2/Build 1624969",
|
|
||||||
"Version 2024.21.0/Build 1631686",
|
|
||||||
"Version 2024.22.0/Build 1645257",
|
|
||||||
"Version 2024.22.1/Build 1652272",
|
|
||||||
"Version 2023.21.0/Build 956283",
|
|
||||||
"Version 2023.22.0/Build 968223",
|
|
||||||
"Version 2023.23.0/Build 983896",
|
|
||||||
"Version 2023.24.0/Build 998541",
|
|
||||||
"Version 2023.25.0/Build 1014750",
|
|
||||||
"Version 2023.25.1/Build 1018737",
|
"Version 2023.25.1/Build 1018737",
|
||||||
"Version 2023.26.0/Build 1019073",
|
"Version 2023.26.0/Build 1019073",
|
||||||
"Version 2023.27.0/Build 1031923",
|
"Version 2023.27.0/Build 1031923",
|
||||||
@ -64,11 +97,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
|||||||
"Version 2023.44.0/Build 1268622",
|
"Version 2023.44.0/Build 1268622",
|
||||||
"Version 2023.45.0/Build 1281371",
|
"Version 2023.45.0/Build 1281371",
|
||||||
"Version 2023.47.0/Build 1303604",
|
"Version 2023.47.0/Build 1303604",
|
||||||
"Version 2022.42.0/Build 638508",
|
"Version 2023.48.0/Build 1319123",
|
||||||
"Version 2022.43.0/Build 648277",
|
"Version 2023.49.0/Build 1321715",
|
||||||
"Version 2022.44.0/Build 664348",
|
"Version 2023.49.1/Build 1322281",
|
||||||
"Version 2022.45.0/Build 677985",
|
"Version 2023.50.0/Build 1332338",
|
||||||
"Version 2023.01.0/Build 709875",
|
"Version 2023.50.1/Build 1345844",
|
||||||
"Version 2023.02.0/Build 717912",
|
"Version 2023.02.0/Build 717912",
|
||||||
"Version 2023.03.0/Build 729220",
|
"Version 2023.03.0/Build 729220",
|
||||||
"Version 2023.04.0/Build 744681",
|
"Version 2023.04.0/Build 744681",
|
||||||
@ -94,11 +127,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
|||||||
"Version 2023.19.0/Build 927681",
|
"Version 2023.19.0/Build 927681",
|
||||||
"Version 2023.20.0/Build 943980",
|
"Version 2023.20.0/Build 943980",
|
||||||
"Version 2023.20.1/Build 946732",
|
"Version 2023.20.1/Build 946732",
|
||||||
"Version 2022.20.0/Build 487703",
|
"Version 2023.21.0/Build 956283",
|
||||||
"Version 2022.21.0/Build 492436",
|
"Version 2023.22.0/Build 968223",
|
||||||
"Version 2022.22.0/Build 498700",
|
"Version 2023.23.0/Build 983896",
|
||||||
"Version 2022.23.0/Build 502374",
|
"Version 2023.24.0/Build 998541",
|
||||||
"Version 2022.23.1/Build 506606",
|
"Version 2023.25.0/Build 1014750",
|
||||||
"Version 2022.24.0/Build 510950",
|
"Version 2022.24.0/Build 510950",
|
||||||
"Version 2022.24.1/Build 513462",
|
"Version 2022.24.1/Build 513462",
|
||||||
"Version 2022.25.0/Build 515072",
|
"Version 2022.25.0/Build 515072",
|
||||||
@ -124,11 +157,11 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
|||||||
"Version 2022.40.0/Build 624782",
|
"Version 2022.40.0/Build 624782",
|
||||||
"Version 2022.41.0/Build 630468",
|
"Version 2022.41.0/Build 630468",
|
||||||
"Version 2022.41.1/Build 634168",
|
"Version 2022.41.1/Build 634168",
|
||||||
"Version 2021.39.1/Build 372418",
|
"Version 2022.42.0/Build 638508",
|
||||||
"Version 2021.41.0/Build 376052",
|
"Version 2022.43.0/Build 648277",
|
||||||
"Version 2021.42.0/Build 378193",
|
"Version 2022.44.0/Build 664348",
|
||||||
"Version 2021.43.0/Build 382019",
|
"Version 2022.45.0/Build 677985",
|
||||||
"Version 2021.44.0/Build 385129",
|
"Version 2023.01.0/Build 709875",
|
||||||
"Version 2021.45.0/Build 387663",
|
"Version 2021.45.0/Build 387663",
|
||||||
"Version 2021.46.0/Build 392043",
|
"Version 2021.46.0/Build 392043",
|
||||||
"Version 2021.47.0/Build 394342",
|
"Version 2021.47.0/Build 394342",
|
||||||
@ -144,7 +177,12 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
|||||||
"Version 2022.17.0/Build 468480",
|
"Version 2022.17.0/Build 468480",
|
||||||
"Version 2022.18.0/Build 473740",
|
"Version 2022.18.0/Build 473740",
|
||||||
"Version 2022.19.1/Build 482464",
|
"Version 2022.19.1/Build 482464",
|
||||||
|
"Version 2022.20.0/Build 487703",
|
||||||
"Version 2022.2.0/Build 405543",
|
"Version 2022.2.0/Build 405543",
|
||||||
|
"Version 2022.21.0/Build 492436",
|
||||||
|
"Version 2022.22.0/Build 498700",
|
||||||
|
"Version 2022.23.0/Build 502374",
|
||||||
|
"Version 2022.23.1/Build 506606",
|
||||||
"Version 2022.3.0/Build 408637",
|
"Version 2022.3.0/Build 408637",
|
||||||
"Version 2022.4.0/Build 411368",
|
"Version 2022.4.0/Build 411368",
|
||||||
"Version 2022.5.0/Build 414731",
|
"Version 2022.5.0/Build 414731",
|
||||||
@ -154,5 +192,44 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[
|
|||||||
"Version 2022.7.0/Build 420849",
|
"Version 2022.7.0/Build 420849",
|
||||||
"Version 2022.8.0/Build 423906",
|
"Version 2022.8.0/Build 423906",
|
||||||
"Version 2022.9.0/Build 426592",
|
"Version 2022.9.0/Build 426592",
|
||||||
|
"Version 2021.17.0/Build 323213",
|
||||||
|
"Version 2021.18.0/Build 324849",
|
||||||
|
"Version 2021.19.0/Build 325762",
|
||||||
|
"Version 2021.20.0/Build 326964",
|
||||||
|
"Version 2021.21.0/Build 327703",
|
||||||
|
"Version 2021.21.1/Build 328461",
|
||||||
|
"Version 2021.22.0/Build 329696",
|
||||||
|
"Version 2021.23.0/Build 331631",
|
||||||
|
"Version 2021.24.0/Build 333951",
|
||||||
|
"Version 2021.25.0/Build 335451",
|
||||||
|
"Version 2021.26.0/Build 336739",
|
||||||
|
"Version 2021.27.0/Build 338857",
|
||||||
|
"Version 2021.28.0/Build 340747",
|
||||||
|
"Version 2021.29.0/Build 342342",
|
||||||
|
"Version 2021.30.0/Build 343820",
|
||||||
|
"Version 2021.31.0/Build 346485",
|
||||||
|
"Version 2021.32.0/Build 349507",
|
||||||
|
"Version 2021.33.0/Build 351843",
|
||||||
|
"Version 2021.34.0/Build 353911",
|
||||||
|
"Version 2021.35.0/Build 355878",
|
||||||
|
"Version 2021.36.0/Build 359254",
|
||||||
|
"Version 2021.36.1/Build 360572",
|
||||||
|
"Version 2021.37.0/Build 361905",
|
||||||
|
"Version 2021.38.0/Build 365032",
|
||||||
|
"Version 2021.39.0/Build 369068",
|
||||||
|
"Version 2021.39.1/Build 372418",
|
||||||
|
"Version 2021.41.0/Build 376052",
|
||||||
|
"Version 2021.42.0/Build 378193",
|
||||||
|
"Version 2021.43.0/Build 382019",
|
||||||
|
"Version 2021.44.0/Build 385129",
|
||||||
|
];
|
||||||
|
pub static _IOS_OS_VERSION_LIST: &[&str; 8] = &[
|
||||||
|
"Version 17.0.1 (Build 21A340)",
|
||||||
|
"Version 17.0.2 (Build 21A350)",
|
||||||
|
"Version 17.0.3 (Build 21A360)",
|
||||||
|
"Version 17.1 (Build 21B74)",
|
||||||
|
"Version 17.1.1 (Build 21B91)",
|
||||||
|
"Version 17.1.2 (Build 21B101)",
|
||||||
|
"Version 17.2 (Build 21C62)",
|
||||||
|
"Version 17.2.1 (Build 21C66)",
|
||||||
];
|
];
|
||||||
pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""];
|
|
||||||
|
40
src/post.rs
@ -1,19 +1,17 @@
|
|||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
// CRATES
|
// CRATES
|
||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::config::get_setting;
|
use crate::config::get_setting;
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_emotes, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
error, format_num, get_filters, nsfw_landing, param, parse_post, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flair, FlairPart, Post, Preferences,
|
||||||
};
|
};
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rinja::Template;
|
use std::collections::HashSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -29,7 +27,7 @@ struct PostTemplate {
|
|||||||
comment_query: String,
|
comment_query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\?q=(.*)&type=comment").unwrap());
|
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap());
|
||||||
|
|
||||||
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
@ -54,7 +52,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
// Log the post ID being fetched in debug mode
|
// Log the post ID being fetched in debug mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
req.param("id").unwrap_or_default();
|
dbg!(req.param("id").unwrap_or_default());
|
||||||
|
|
||||||
let single_thread = req.param("comment_id").is_some();
|
let single_thread = req.param("comment_id").is_some();
|
||||||
let highlighted_comment = &req.param("comment_id").unwrap_or_default();
|
let highlighted_comment = &req.param("comment_id").unwrap_or_default();
|
||||||
@ -74,22 +72,18 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
|
return Ok(nsfw_landing(req, req_url).await.unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let query_body = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||||
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
|
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace('+', " "),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let query_string = format!("q={query_body}&type=comment");
|
|
||||||
let form = url::form_urlencoded::parse(query_string.as_bytes()).collect::<HashMap<_, _>>();
|
|
||||||
let query = form.get("q").unwrap().clone().to_string();
|
|
||||||
|
|
||||||
let comments = match query.as_str() {
|
let comments = match query.as_str() {
|
||||||
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
|
"" => 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),
|
_ => 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
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
Ok(template(&PostTemplate {
|
template(PostTemplate {
|
||||||
comments,
|
comments,
|
||||||
post,
|
post,
|
||||||
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
|
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
|
||||||
@ -98,15 +92,15 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
single_thread,
|
single_thread,
|
||||||
url: req_url,
|
url: req_url,
|
||||||
comment_query: query,
|
comment_query: query,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" || msg == "gated" {
|
if msg == "quarantined" || msg == "gated" {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
Ok(quarantine(&req, sub, &msg))
|
quarantine(req, sub, msg)
|
||||||
} else {
|
} else {
|
||||||
error(req, &msg).await
|
error(req, msg).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,19 +139,19 @@ fn query_comments(
|
|||||||
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
for comment in comments {
|
comments.into_iter().for_each(|comment| {
|
||||||
let data = &comment["data"];
|
let data = &comment["data"];
|
||||||
|
|
||||||
// If this comment contains replies, handle those too
|
// If this comment contains replies, handle those too
|
||||||
if data["replies"].is_object() {
|
if data["replies"].is_object() {
|
||||||
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req));
|
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req))
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
|
let c = build_comment(&comment, data, Vec::new(), post_link, post_author, highlighted_comment, filters, req);
|
||||||
if c.body.to_lowercase().contains(&query.to_lowercase()) {
|
if c.body.to_lowercase().contains(&query.to_lowercase()) {
|
||||||
results.push(c);
|
results.push(c);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
@ -176,11 +170,13 @@ fn build_comment(
|
|||||||
|
|
||||||
let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" {
|
let body = if (val(comment, "author") == "[deleted]" && val(comment, "body") == "[removed]") || val(comment, "body") == "[ Removed by Reddit ]" {
|
||||||
format!(
|
format!(
|
||||||
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{post_link}{id}\">view removed comment</a></p></div>",
|
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}{}\">view removed comment</a></p></div>",
|
||||||
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
||||||
|
post_link,
|
||||||
|
id
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
rewrite_emotes(&data["media_metadata"], val(comment, "body_html"))
|
rewrite_urls(&val(comment, "body_html"))
|
||||||
};
|
};
|
||||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
use std::{collections::HashMap, fmt::Display, io::Write};
|
|
||||||
|
|
||||||
use clap::{Parser, ValueEnum};
|
|
||||||
use common_words_all::{get_top, Language, NgramSize};
|
|
||||||
use redsunlib::utils::Post;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "my_cli")]
|
|
||||||
#[command(about = "A simple CLI example", long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
#[arg(short = 's', long = "sub")]
|
|
||||||
sub: String,
|
|
||||||
|
|
||||||
#[arg(long = "sort")]
|
|
||||||
sort: SortOrder,
|
|
||||||
|
|
||||||
#[arg(short = 'f', long = "format", value_enum)]
|
|
||||||
format: Format,
|
|
||||||
#[arg(short = 'o', long = "output")]
|
|
||||||
output: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, ValueEnum)]
|
|
||||||
enum SortOrder {
|
|
||||||
Hot,
|
|
||||||
Rising,
|
|
||||||
New,
|
|
||||||
Top,
|
|
||||||
Controversial,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for SortOrder {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
SortOrder::Hot => write!(f, "hot"),
|
|
||||||
SortOrder::Rising => write!(f, "rising"),
|
|
||||||
SortOrder::New => write!(f, "new"),
|
|
||||||
SortOrder::Top => write!(f, "top"),
|
|
||||||
SortOrder::Controversial => write!(f, "controversial"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, ValueEnum)]
|
|
||||||
enum Format {
|
|
||||||
Json,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
pretty_env_logger::init();
|
|
||||||
let cli = Cli::parse();
|
|
||||||
let (sub, sort, format, output) = (cli.sub, cli.sort, cli.format, cli.output);
|
|
||||||
let initial = format!("/r/{sub}/{sort}.json?&raw_json=1");
|
|
||||||
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
|
|
||||||
let mut hashmap = HashMap::new();
|
|
||||||
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
|
|
||||||
loop {
|
|
||||||
print!("\r");
|
|
||||||
let path = format!("/r/{sub}/{sort}.json?sort={sort}&t=&after={after}&raw_json=1");
|
|
||||||
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
|
|
||||||
let old_len = hashmap.len();
|
|
||||||
// convert to hashmap and extend hashmap
|
|
||||||
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
|
|
||||||
let len = new_posts.len();
|
|
||||||
hashmap.extend(new_posts);
|
|
||||||
if hashmap.len() - old_len < 3 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x = hashmap.len() - old_len;
|
|
||||||
after = new_after;
|
|
||||||
// Print number of posts fetched
|
|
||||||
print!("Fetched {len} posts (+{x})",);
|
|
||||||
std::io::stdout().flush().unwrap();
|
|
||||||
}
|
|
||||||
println!("\n\n");
|
|
||||||
// additionally search if final count not reached
|
|
||||||
|
|
||||||
for word in get_top(Language::English, 10_000, NgramSize::One) {
|
|
||||||
let mut retrieved_posts_from_search = 0;
|
|
||||||
let initial = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}");
|
|
||||||
println!("Grabbing posts with word {word}.");
|
|
||||||
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
|
|
||||||
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
|
|
||||||
'search: loop {
|
|
||||||
let path = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}&after={after}");
|
|
||||||
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
|
|
||||||
if new_posts.is_empty() || new_after.is_empty() {
|
|
||||||
println!("No more posts for word {word}");
|
|
||||||
break 'search;
|
|
||||||
}
|
|
||||||
retrieved_posts_from_search += new_posts.len();
|
|
||||||
let old_len = hashmap.len();
|
|
||||||
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
|
|
||||||
let len = new_posts.len();
|
|
||||||
hashmap.extend(new_posts);
|
|
||||||
let delta = hashmap.len() - old_len;
|
|
||||||
after = new_after;
|
|
||||||
// Print number of posts fetched
|
|
||||||
println!("Fetched {len} posts (+{delta})",);
|
|
||||||
|
|
||||||
if retrieved_posts_from_search > 1000 {
|
|
||||||
println!("Reached 1000 posts from search");
|
|
||||||
break 'search;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Need to save incrementally. atomic save + move
|
|
||||||
let tmp_file = output.clone().unwrap_or_else(|| format!("{sub}.json.tmp"));
|
|
||||||
let perm_file = output.clone().unwrap_or_else(|| format!("{sub}.json"));
|
|
||||||
write_posts(&hashmap.values().collect(), tmp_file.clone());
|
|
||||||
// move file
|
|
||||||
std::fs::rename(tmp_file, perm_file).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n\n");
|
|
||||||
|
|
||||||
println!("Size of hashmap: {}", hashmap.len());
|
|
||||||
|
|
||||||
let posts: Vec<&Post> = hashmap.values().collect();
|
|
||||||
match format {
|
|
||||||
Format::Json => {
|
|
||||||
let filename: String = output.unwrap_or_else(|| format!("{sub}.json"));
|
|
||||||
write_posts(&posts, filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_posts(posts: &Vec<&Post>, filename: String) {
|
|
||||||
let json = serde_json::to_string(&posts).unwrap();
|
|
||||||
std::fs::write(filename, json).unwrap();
|
|
||||||
}
|
|
@ -1,16 +1,14 @@
|
|||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
|
use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
|
||||||
use crate::{
|
use crate::{
|
||||||
client::json,
|
client::json,
|
||||||
server::RequestExt,
|
|
||||||
subreddit::{can_access_quarantine, quarantine},
|
subreddit::{can_access_quarantine, quarantine},
|
||||||
|
RequestExt,
|
||||||
};
|
};
|
||||||
|
use askama::Template;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rinja::Template;
|
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
struct SearchParams {
|
struct SearchParams {
|
||||||
@ -62,21 +60,16 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let uri_path = req.uri().path().replace("+", "%2B");
|
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||||
let path = format!("{}.json?{}{}&raw_json=1", uri_path, req.uri().query().unwrap_or_default(), nsfw_results);
|
|
||||||
let mut query = param(&path, "q").unwrap_or_default();
|
let mut query = param(&path, "q").unwrap_or_default();
|
||||||
query = REDDIT_URL_MATCH.replace(&query, "").to_string();
|
query = REDDIT_URL_MATCH.replace(&query, "").to_string();
|
||||||
|
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return Ok(redirect("/"));
|
return Ok(redirect("/".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.starts_with("r/") || query.starts_with("user/") {
|
if query.starts_with("r/") {
|
||||||
return Ok(redirect(&format!("/{query}")));
|
return Ok(redirect(format!("/{}", query)));
|
||||||
}
|
|
||||||
|
|
||||||
if query.starts_with("u/") {
|
|
||||||
return Ok(redirect(&format!("/user{}", &query[1..])));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
@ -104,7 +97,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
// If all requested subs are filtered, we don't need to fetch posts.
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
if sub.split('+').all(|s| filters.contains(s)) {
|
if sub.split('+').all(|s| filters.contains(s)) {
|
||||||
Ok(template(&SearchTemplate {
|
template(SearchTemplate {
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
subreddits,
|
subreddits,
|
||||||
sub,
|
sub,
|
||||||
@ -113,7 +106,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
sort,
|
sort,
|
||||||
t: param(&path, "t").unwrap_or_default(),
|
t: param(&path, "t").unwrap_or_default(),
|
||||||
before: param(&path, "after").unwrap_or_default(),
|
before: param(&path, "after").unwrap_or_default(),
|
||||||
after: String::new(),
|
after: "".to_string(),
|
||||||
restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
|
restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
|
||||||
typed,
|
typed,
|
||||||
},
|
},
|
||||||
@ -123,14 +116,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered: false,
|
all_posts_filtered: false,
|
||||||
all_posts_hidden_nsfw: false,
|
all_posts_hidden_nsfw: false,
|
||||||
no_posts: false,
|
no_posts: false,
|
||||||
}))
|
})
|
||||||
} else {
|
} else {
|
||||||
match Post::fetch(&path, quarantined).await {
|
match Post::fetch(&path, quarantined).await {
|
||||||
Ok((mut posts, after)) => {
|
Ok((mut posts, after)) => {
|
||||||
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
||||||
let no_posts = posts.is_empty();
|
let no_posts = posts.is_empty();
|
||||||
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
||||||
Ok(template(&SearchTemplate {
|
template(SearchTemplate {
|
||||||
posts,
|
posts,
|
||||||
subreddits,
|
subreddits,
|
||||||
sub,
|
sub,
|
||||||
@ -149,14 +142,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
all_posts_hidden_nsfw,
|
all_posts_hidden_nsfw,
|
||||||
no_posts,
|
no_posts,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" || msg == "gated" {
|
if msg == "quarantined" || msg == "gated" {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
Ok(quarantine(&req, sub, &msg))
|
quarantine(req, sub, msg)
|
||||||
} else {
|
} else {
|
||||||
error(req, &msg).await
|
error(req, msg).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,7 +158,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
|
async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
|
||||||
let limit = if typed == "sr_user" { "50" } else { "3" };
|
let limit = if typed == "sr_user" { "50" } else { "3" };
|
||||||
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit={limit}", q.replace(' ', "+"));
|
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit={}", q.replace(' ', "+"), limit);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
|
json(subreddit_search_path, false).await.unwrap_or_default()["data"]["children"]
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
|
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
@ -18,7 +15,6 @@ use libflate::gzip;
|
|||||||
use route_recognizer::{Params, Router};
|
use route_recognizer::{Params, Router};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
fmt::Display,
|
|
||||||
io,
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
result::Result,
|
result::Result,
|
||||||
@ -69,12 +65,12 @@ impl CompressionType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for CompressionType {
|
impl ToString for CompressionType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn to_string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Gzip => write!(f, "gzip"),
|
Self::Gzip => "gzip".to_string(),
|
||||||
Self::Brotli => write!(f, "br"),
|
Self::Brotli => "br".to_string(),
|
||||||
Self::Passthrough => Ok(()),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,13 +104,13 @@ pub trait RequestExt {
|
|||||||
fn params(&self) -> Params;
|
fn params(&self) -> Params;
|
||||||
fn param(&self, name: &str) -> Option<String>;
|
fn param(&self, name: &str) -> Option<String>;
|
||||||
fn set_params(&mut self, params: Params) -> Option<Params>;
|
fn set_params(&mut self, params: Params) -> Option<Params>;
|
||||||
fn cookies(&self) -> Vec<Cookie<'_>>;
|
fn cookies(&self) -> Vec<Cookie>;
|
||||||
fn cookie(&self, name: &str) -> Option<Cookie<'_>>;
|
fn cookie(&self, name: &str) -> Option<Cookie>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ResponseExt {
|
pub trait ResponseExt {
|
||||||
fn cookies(&self) -> Vec<Cookie<'_>>;
|
fn cookies(&self) -> Vec<Cookie>;
|
||||||
fn insert_cookie(&mut self, cookie: Cookie<'_>);
|
fn insert_cookie(&mut self, cookie: Cookie);
|
||||||
fn remove_cookie(&mut self, name: String);
|
fn remove_cookie(&mut self, name: String);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +131,7 @@ impl RequestExt for Request<Body> {
|
|||||||
self.extensions_mut().insert(params)
|
self.extensions_mut().insert(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cookies(&self) -> Vec<Cookie<'_>> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
header
|
header
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -146,13 +142,13 @@ impl RequestExt for Request<Body> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cookie(&self, name: &str) -> Option<Cookie<'_>> {
|
fn cookie(&self, name: &str) -> Option<Cookie> {
|
||||||
self.cookies().into_iter().find(|c| c.name() == name)
|
self.cookies().into_iter().find(|c| c.name() == name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseExt for Response<Body> {
|
impl ResponseExt for Response<Body> {
|
||||||
fn cookies(&self) -> Vec<Cookie<'_>> {
|
fn cookies(&self) -> Vec<Cookie> {
|
||||||
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
self.headers().get("Cookie").map_or(Vec::new(), |header| {
|
||||||
header
|
header
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -163,7 +159,7 @@ impl ResponseExt for Response<Body> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_cookie(&mut self, cookie: Cookie<'_>) {
|
fn insert_cookie(&mut self, cookie: Cookie) {
|
||||||
if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) {
|
if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) {
|
||||||
self.headers_mut().append("Set-Cookie", val);
|
self.headers_mut().append("Set-Cookie", val);
|
||||||
}
|
}
|
||||||
@ -180,25 +176,19 @@ impl ResponseExt for Response<Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Route<'_> {
|
impl Route<'_> {
|
||||||
fn method(&mut self, method: &Method, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
fn method(&mut self, method: Method, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
||||||
self.router.add(&format!("/{}{}", method.as_str(), self.path), dest);
|
self.router.add(&format!("/{}{}", method.as_str(), self.path), dest);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an endpoint for `GET` requests
|
/// Add an endpoint for `GET` requests
|
||||||
pub fn get(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
pub fn get(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
||||||
self.method(&Method::GET, dest)
|
self.method(Method::GET, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an endpoint for `POST` requests
|
/// Add an endpoint for `POST` requests
|
||||||
pub fn post(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
pub fn post(&mut self, dest: fn(Request<Body>) -> BoxResponse) -> &mut Self {
|
||||||
self.method(&Method::POST, dest)
|
self.method(Method::POST, dest)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Server {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,14 +200,14 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn at(&mut self, path: &str) -> Route<'_> {
|
pub fn at(&mut self, path: &str) -> Route {
|
||||||
Route {
|
Route {
|
||||||
path: path.to_owned(),
|
path: path.to_owned(),
|
||||||
router: &mut self.router,
|
router: &mut self.router,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen(self, addr: &str) -> Boxed<Result<(), hyper::Error>> {
|
pub fn listen(self, addr: String) -> Boxed<Result<(), hyper::Error>> {
|
||||||
let make_svc = make_service_fn(move |_conn| {
|
let make_svc = make_service_fn(move |_conn| {
|
||||||
// For correct borrowing, these values need to be borrowed
|
// For correct borrowing, these values need to be borrowed
|
||||||
let router = self.router.clone();
|
let router = self.router.clone();
|
||||||
@ -270,7 +260,7 @@ impl Server {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build SocketAddr from provided address
|
// Build SocketAddr from provided address
|
||||||
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {addr} as address (example format: 0.0.0.0:8080)"));
|
let address = &addr.parse().unwrap_or_else(|_| panic!("Cannot parse {} as address (example format: 0.0.0.0:8080)", addr));
|
||||||
|
|
||||||
// Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
|
// Bind server to address specified above. Gracefully shut down if CTRL+C is pressed
|
||||||
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
|
let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async {
|
||||||
@ -386,7 +376,7 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
|
|||||||
|
|
||||||
// The compressor and q-value (if the latter is defined)
|
// The compressor and q-value (if the latter is defined)
|
||||||
// will be delimited by semicolons.
|
// will be delimited by semicolons.
|
||||||
let mut spl: Split<'_, char> = val.split(';');
|
let mut spl: Split<char> = val.split(';');
|
||||||
|
|
||||||
// Get the compressor. For example, in
|
// Get the compressor. For example, in
|
||||||
// gzip;q=0.8
|
// gzip;q=0.8
|
||||||
@ -448,10 +438,10 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if cur_candidate.q == f64::NEG_INFINITY {
|
if cur_candidate.q != f64::NEG_INFINITY {
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(cur_candidate.alg)
|
Some(cur_candidate.alg)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,16 +453,16 @@ fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
|
|||||||
/// conditions are met:
|
/// conditions are met:
|
||||||
///
|
///
|
||||||
/// 1. the HTTP client requests a compression encoding in the Content-Encoding
|
/// 1. the HTTP client requests a compression encoding in the Content-Encoding
|
||||||
/// header (hence the need for the `req_headers`);
|
/// header (hence the need for the req_headers);
|
||||||
///
|
///
|
||||||
/// 2. the content encoding corresponds to a compression algorithm we support;
|
/// 2. the content encoding corresponds to a compression algorithm we support;
|
||||||
///
|
///
|
||||||
/// 3. the Media type in the Content-Type response header is text with any
|
/// 3. the Media type in the Content-Type response header is text with any
|
||||||
/// subtype (e.g. text/plain) or application/json.
|
/// subtype (e.g. text/plain) or application/json.
|
||||||
///
|
///
|
||||||
/// `compress_response` returns Ok on successful compression, or if not all three
|
/// compress_response returns Ok on successful compression, or if not all three
|
||||||
/// conditions above are met. It returns Err if there was a problem decoding
|
/// conditions above are met. It returns Err if there was a problem decoding
|
||||||
/// any header in either `req_headers` or res, but res will remain intact.
|
/// any header in either req_headers or res, but res will remain intact.
|
||||||
///
|
///
|
||||||
/// This function logs errors to stderr, but only in debug mode. No information
|
/// This function logs errors to stderr, but only in debug mode. No information
|
||||||
/// is logged in release builds.
|
/// is logged in release builds.
|
||||||
@ -611,7 +601,7 @@ fn compress_body(compressor: CompressionType, body_bytes: Vec<u8>) -> Result<Vec
|
|||||||
|
|
||||||
// This arm is for any requested compressor for which we don't yet
|
// This arm is for any requested compressor for which we don't yet
|
||||||
// have an implementation.
|
// have an implementation.
|
||||||
CompressionType::Passthrough => {
|
_ => {
|
||||||
let msg = "unsupported compressor".to_string();
|
let msg = "unsupported compressor".to_string();
|
||||||
return Err(msg);
|
return Err(msg);
|
||||||
}
|
}
|
||||||
@ -687,7 +677,7 @@ mod tests {
|
|||||||
|
|
||||||
// Perform the compression.
|
// Perform the compression.
|
||||||
if let Err(e) = block_on(compress_response(&req_headers, &mut res)) {
|
if let Err(e) = block_on(compress_response(&req_headers, &mut res)) {
|
||||||
panic!("compress_response(&req_headers, &mut res) => Err(\"{e}\")");
|
panic!("compress_response(&req_headers, &mut res) => Err(\"{}\")", e);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the content was compressed, we expect the Content-Encoding
|
// If the content was compressed, we expect the Content-Encoding
|
||||||
@ -709,7 +699,7 @@ mod tests {
|
|||||||
// the Response is the same as what with which we start.
|
// the Response is the same as what with which we start.
|
||||||
let body_vec = match block_on(body::to_bytes(res.body_mut())) {
|
let body_vec = match block_on(body::to_bytes(res.body_mut())) {
|
||||||
Ok(b) => b.to_vec(),
|
Ok(b) => b.to_vec(),
|
||||||
Err(e) => panic!("{e}"),
|
Err(e) => panic!("{}", e),
|
||||||
};
|
};
|
||||||
|
|
||||||
if expected_encoding == CompressionType::Passthrough {
|
if expected_encoding == CompressionType::Passthrough {
|
||||||
@ -725,17 +715,17 @@ mod tests {
|
|||||||
let mut decoder: Box<dyn io::Read> = match expected_encoding {
|
let mut decoder: Box<dyn io::Read> = match expected_encoding {
|
||||||
CompressionType::Gzip => match gzip::Decoder::new(&mut body_cursor) {
|
CompressionType::Gzip => match gzip::Decoder::new(&mut body_cursor) {
|
||||||
Ok(dgz) => Box::new(dgz),
|
Ok(dgz) => Box::new(dgz),
|
||||||
Err(e) => panic!("{e}"),
|
Err(e) => panic!("{}", e),
|
||||||
},
|
},
|
||||||
|
|
||||||
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
|
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
|
||||||
|
|
||||||
_ => panic!("no decompressor for {}", expected_encoding),
|
_ => panic!("no decompressor for {}", expected_encoding.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut decompressed = Vec::<u8>::new();
|
let mut decompressed = Vec::<u8>::new();
|
||||||
if let Err(e) = io::copy(&mut decoder, &mut decompressed) {
|
if let Err(e) = io::copy(&mut decoder, &mut decompressed) {
|
||||||
panic!("{e}");
|
panic!("{}", e);
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(decompressed.eq(&expected_lorem_ipsum));
|
assert!(decompressed.eq(&expected_lorem_ipsum));
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// CRATES
|
// CRATES
|
||||||
use crate::server::ResponseExt;
|
use crate::server::ResponseExt;
|
||||||
use crate::utils::{redirect, template, Preferences};
|
use crate::utils::{redirect, template, Preferences};
|
||||||
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use futures_lite::StreamExt;
|
use futures_lite::StreamExt;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use rinja::Template;
|
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -21,27 +19,22 @@ struct SettingsTemplate {
|
|||||||
|
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
|
|
||||||
const PREFS: [&str; 20] = [
|
const PREFS: [&str; 15] = [
|
||||||
"theme",
|
"theme",
|
||||||
"mascot",
|
|
||||||
"front_page",
|
"front_page",
|
||||||
"layout",
|
"layout",
|
||||||
"wide",
|
"wide",
|
||||||
"comment_sort",
|
"comment_sort",
|
||||||
"post_sort",
|
"post_sort",
|
||||||
"blur_spoiler",
|
|
||||||
"show_nsfw",
|
"show_nsfw",
|
||||||
"blur_nsfw",
|
"blur_nsfw",
|
||||||
"use_hls",
|
"use_hls",
|
||||||
"ffmpeg_video_downloads",
|
|
||||||
"hide_hls_notification",
|
"hide_hls_notification",
|
||||||
"autoplay_videos",
|
"autoplay_videos",
|
||||||
"hide_sidebar_and_summary",
|
|
||||||
"fixed_navbar",
|
"fixed_navbar",
|
||||||
"hide_awards",
|
"hide_awards",
|
||||||
"hide_score",
|
"hide_score",
|
||||||
"disable_visit_reddit_confirmation",
|
"disable_visit_reddit_confirmation",
|
||||||
"video_quality",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
@ -49,10 +42,10 @@ const PREFS: [&str; 20] = [
|
|||||||
// Retrieve cookies from request "Cookie" header
|
// Retrieve cookies from request "Cookie" header
|
||||||
pub async fn get(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn get(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let url = req.uri().to_string();
|
let url = req.uri().to_string();
|
||||||
Ok(template(&SettingsTemplate {
|
template(SettingsTemplate {
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url,
|
url,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cookies using response "Set-Cookie" header
|
// Set cookies using response "Set-Cookie" header
|
||||||
@ -61,7 +54,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let (parts, mut body) = req.into_parts();
|
let (parts, mut body) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let _cookies: Vec<Cookie<'_>> = parts
|
let _cookies: Vec<Cookie> = parts
|
||||||
.headers
|
.headers
|
||||||
.get_all("Cookie")
|
.get_all("Cookie")
|
||||||
.iter()
|
.iter()
|
||||||
@ -80,14 +73,14 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
|
let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let mut response = redirect("/settings");
|
let mut response = redirect("/settings".to_string());
|
||||||
|
|
||||||
for &name in &PREFS {
|
for &name in &PREFS {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => response.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build((name.to_owned(), value.clone()))
|
Cookie::build((name.to_owned(), value.clone()))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(name != "ffmpeg_video_downloads")
|
.http_only(true)
|
||||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
@ -103,7 +96,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
|||||||
let (parts, _) = req.into_parts();
|
let (parts, _) = req.into_parts();
|
||||||
|
|
||||||
// Grab existing cookies
|
// Grab existing cookies
|
||||||
let _cookies: Vec<Cookie<'_>> = parts
|
let _cookies: Vec<Cookie> = parts
|
||||||
.headers
|
.headers
|
||||||
.get_all("Cookie")
|
.get_all("Cookie")
|
||||||
.iter()
|
.iter()
|
||||||
@ -119,14 +112,14 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
|||||||
None => "/".to_string(),
|
None => "/".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut response = redirect(&path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() {
|
for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => response.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build((name.to_owned(), value.clone()))
|
Cookie::build((name.to_owned(), value.clone()))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(name != "ffmpeg_video_downloads")
|
.http_only(true)
|
||||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
|
164
src/subreddit.rs
@ -1,17 +1,12 @@
|
|||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
use crate::{config, utils};
|
|
||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
||||||
};
|
};
|
||||||
use crate::{client::json, server::RequestExt, server::ResponseExt};
|
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||||
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use rinja::Template;
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -55,13 +50,10 @@ struct WallTemplate {
|
|||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
static GEO_FILTER_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"geo_filter=(?<region>\w+)").unwrap());
|
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let root = req.uri().path() == "/";
|
let root = req.uri().path() == "/";
|
||||||
let query = req.uri().query().unwrap_or_default().to_string();
|
|
||||||
let subscribed = setting(&req, "subscriptions");
|
let subscribed = setting(&req, "subscriptions");
|
||||||
let front_page = setting(&req, "front_page");
|
let front_page = setting(&req, "front_page");
|
||||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||||
@ -84,7 +76,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.param("sub").is_some() && sub_name.starts_with("u_") {
|
if req.param("sub").is_some() && sub_name.starts_with("u_") {
|
||||||
return Ok(redirect(&["/user/", &sub_name[2..]].concat()));
|
return Ok(redirect(["/user/", &sub_name[2..]].concat()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request subreddit metadata
|
// Request subreddit metadata
|
||||||
@ -115,25 +107,21 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
let mut params = String::from("&raw_json=1");
|
let mut params = String::from("&raw_json=1");
|
||||||
if sub_name == "popular" {
|
if sub_name == "popular" {
|
||||||
let geo_filter = match GEO_FILTER_MATCH.captures(&query) {
|
params.push_str("&geo_filter=GLOBAL");
|
||||||
Some(geo_filter) => geo_filter["region"].to_string(),
|
|
||||||
None => "GLOBAL".to_owned(),
|
|
||||||
};
|
|
||||||
params.push_str(&format!("&geo_filter={geo_filter}"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = format!("/r/{}/{sort}.json?{}{params}", sub_name.replace('+', "%2B"), req.uri().query().unwrap_or_default());
|
let path = format!("/r/{sub_name}/{sort}.json?{}{params}", req.uri().query().unwrap_or_default());
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
|
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
|
||||||
let filters = get_filters(&req);
|
let filters = get_filters(&req);
|
||||||
|
|
||||||
// If all requested subs are filtered, we don't need to fetch posts.
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
if sub_name.split('+').all(|s| filters.contains(s)) {
|
if sub_name.split('+').all(|s| filters.contains(s)) {
|
||||||
Ok(template(&SubredditTemplate {
|
template(SubredditTemplate {
|
||||||
sub,
|
sub,
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after").unwrap_or_default(), String::new()),
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url,
|
url,
|
||||||
redirect_url,
|
redirect_url,
|
||||||
@ -141,18 +129,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered: false,
|
all_posts_filtered: false,
|
||||||
all_posts_hidden_nsfw: false,
|
all_posts_hidden_nsfw: false,
|
||||||
no_posts: false,
|
no_posts: false,
|
||||||
}))
|
})
|
||||||
} else {
|
} else {
|
||||||
match Post::fetch(&path, quarantined).await {
|
match Post::fetch(&path, quarantined).await {
|
||||||
Ok((mut posts, after)) => {
|
Ok((mut posts, after)) => {
|
||||||
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
||||||
let no_posts = posts.is_empty();
|
let no_posts = posts.is_empty();
|
||||||
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
||||||
if sort == "new" {
|
template(SubredditTemplate {
|
||||||
posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts));
|
|
||||||
posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied));
|
|
||||||
}
|
|
||||||
Ok(template(&SubredditTemplate {
|
|
||||||
sub,
|
sub,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
@ -164,38 +148,40 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
all_posts_hidden_nsfw,
|
all_posts_hidden_nsfw,
|
||||||
no_posts,
|
no_posts,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
Err(msg) => match msg.as_str() {
|
Err(msg) => match msg.as_str() {
|
||||||
"quarantined" | "gated" => Ok(quarantine(&req, sub_name, &msg)),
|
"quarantined" | "gated" => quarantine(req, sub_name, msg),
|
||||||
"private" => error(req, &format!("r/{sub_name} is a private community")).await,
|
"private" => error(req, format!("r/{} is a private community", sub_name)).await,
|
||||||
"banned" => error(req, &format!("r/{sub_name} has been banned from Reddit")).await,
|
"banned" => error(req, format!("r/{} has been banned from Reddit", sub_name)).await,
|
||||||
_ => error(req, &msg).await,
|
_ => error(req, msg).await,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quarantine(req: &Request<Body>, sub: String, restriction: &str) -> Response<Body> {
|
pub fn quarantine(req: Request<Body>, sub: String, restriction: String) -> Result<Response<Body>, String> {
|
||||||
let wall = WallTemplate {
|
let wall = WallTemplate {
|
||||||
title: format!("r/{sub} is {restriction}"),
|
title: format!("r/{} is {}", sub, restriction),
|
||||||
msg: "Please click the button below to continue to this subreddit.".to_string(),
|
msg: "Please click the button below to continue to this subreddit.".to_string(),
|
||||||
url: req.uri().to_string(),
|
url: req.uri().to_string(),
|
||||||
sub,
|
sub,
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(&req),
|
||||||
};
|
};
|
||||||
|
|
||||||
Response::builder()
|
Ok(
|
||||||
.status(403)
|
Response::builder()
|
||||||
.header("content-type", "text/html")
|
.status(403)
|
||||||
.body(wall.render().unwrap_or_default().into())
|
.header("content-type", "text/html")
|
||||||
.unwrap_or_default()
|
.body(wall.render().unwrap_or_default().into())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn add_quarantine_exception(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let subreddit = req.param("sub").ok_or("Invalid URL")?;
|
let subreddit = req.param("sub").ok_or("Invalid URL")?;
|
||||||
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
|
let redir = param(&format!("?{}", req.uri().query().unwrap_or_default()), "redir").ok_or("Invalid URL")?;
|
||||||
let mut response = redirect(&redir);
|
let mut response = redirect(redir);
|
||||||
response.insert_cookie(
|
response.insert_cookie(
|
||||||
Cookie::build((&format!("allow_quaran_{}", subreddit.to_lowercase()), "true"))
|
Cookie::build((&format!("allow_quaran_{}", subreddit.to_lowercase()), "true"))
|
||||||
.path("/")
|
.path("/")
|
||||||
@ -220,8 +206,9 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
if sub == "random" || sub == "randnsfw" {
|
if sub == "random" || sub == "randnsfw" {
|
||||||
if action.contains(&"filter".to_string()) || action.contains(&"unfilter".to_string()) {
|
if action.contains(&"filter".to_string()) || action.contains(&"unfilter".to_string()) {
|
||||||
return Err("Can't filter random subreddit!".to_string());
|
return Err("Can't filter random subreddit!".to_string());
|
||||||
|
} else {
|
||||||
|
return Err("Can't subscribe to random subreddit!".to_string());
|
||||||
}
|
}
|
||||||
return Err("Can't subscribe to random subreddit!".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default().to_string();
|
let query = req.uri().query().unwrap_or_default().to_string();
|
||||||
@ -232,7 +219,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
|
|
||||||
// Retrieve list of posts for these subreddits to extract display names
|
// Retrieve list of posts for these subreddits to extract display names
|
||||||
|
|
||||||
let posts = json(format!("/r/{sub}/hot.json?raw_json=1"), true).await;
|
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await;
|
||||||
let display_lookup: Vec<(String, &str)> = match &posts {
|
let display_lookup: Vec<(String, &str)> = match &posts {
|
||||||
Ok(posts) => posts["data"]["children"]
|
Ok(posts) => posts["data"]["children"]
|
||||||
.as_array()
|
.as_array()
|
||||||
@ -260,7 +247,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
display
|
display
|
||||||
} else {
|
} else {
|
||||||
// This subreddit display name isn't known, retrieve it
|
// This subreddit display name isn't known, retrieve it
|
||||||
let path: String = format!("/r/{part}/about.json?raw_json=1");
|
let path: String = format!("/r/{}/about.json?raw_json=1", part);
|
||||||
display = json(path, true).await;
|
display = json(path, true).await;
|
||||||
match &display {
|
match &display {
|
||||||
Ok(display) => display["data"]["display_name"].as_str(),
|
Ok(display) => display["data"]["display_name"].as_str(),
|
||||||
@ -295,13 +282,13 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
|
|
||||||
// Redirect back to subreddit
|
// Redirect back to subreddit
|
||||||
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
||||||
let path = if let Some(redirect_path) = param(&format!("?{query}"), "redirect") {
|
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
||||||
format!("/{redirect_path}")
|
format!("/{}", redirect_path)
|
||||||
} else {
|
} else {
|
||||||
format!("/r/{sub}")
|
format!("/r/{}", sub)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut response = redirect(&path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
// Delete cookie if empty, else set
|
// Delete cookie if empty, else set
|
||||||
if sub_list.is_empty() {
|
if sub_list.is_empty() {
|
||||||
@ -339,22 +326,22 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
||||||
let path: String = format!("/r/{sub}/wiki/{page}.json?raw_json=1");
|
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||||
let url = req.uri().to_string();
|
let url = req.uri().to_string();
|
||||||
|
|
||||||
match json(path, quarantined).await {
|
match json(path, quarantined).await {
|
||||||
Ok(response) => Ok(template(&WikiTemplate {
|
Ok(response) => template(WikiTemplate {
|
||||||
sub,
|
sub,
|
||||||
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
|
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
|
||||||
page,
|
page,
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url,
|
url,
|
||||||
})),
|
}),
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" || msg == "gated" {
|
if msg == "quarantined" || msg == "gated" {
|
||||||
Ok(quarantine(&req, sub, &msg))
|
quarantine(req, sub, msg)
|
||||||
} else {
|
} else {
|
||||||
error(req, &msg).await
|
error(req, msg).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,13 +357,13 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API url
|
||||||
let path: String = format!("/r/{sub}/about.json?raw_json=1");
|
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||||
let url = req.uri().to_string();
|
let url = req.uri().to_string();
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match json(path, quarantined).await {
|
match json(path, quarantined).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => Ok(template(&WikiTemplate {
|
Ok(response) => template(WikiTemplate {
|
||||||
wiki: rewrite_urls(&val(&response, "description_html")),
|
wiki: rewrite_urls(&val(&response, "description_html")),
|
||||||
// wiki: format!(
|
// wiki: format!(
|
||||||
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
||||||
@ -387,12 +374,12 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
page: "Sidebar".to_string(),
|
page: "Sidebar".to_string(),
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url,
|
url,
|
||||||
})),
|
}),
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" || msg == "gated" {
|
if msg == "quarantined" || msg == "gated" {
|
||||||
Ok(quarantine(&req, sub, &msg))
|
quarantine(req, sub, msg)
|
||||||
} else {
|
} else {
|
||||||
error(req, &msg).await
|
error(req, msg).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -435,7 +422,7 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
// SUBREDDIT
|
// SUBREDDIT
|
||||||
async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API url
|
||||||
let path: String = format!("/r/{sub}/about.json?raw_json=1");
|
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
let res = json(path, quarantined).await?;
|
let res = json(path, quarantined).await?;
|
||||||
@ -462,71 +449,8 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
|
|
||||||
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
|
|
||||||
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
|
|
||||||
}
|
|
||||||
|
|
||||||
use hyper::header::CONTENT_TYPE;
|
|
||||||
use rss::{ChannelBuilder, Item};
|
|
||||||
|
|
||||||
// Get subreddit
|
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
|
||||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
|
||||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
|
||||||
|
|
||||||
// Get path
|
|
||||||
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
|
|
||||||
|
|
||||||
// Get subreddit data
|
|
||||||
let subreddit = subreddit(&sub, false).await?;
|
|
||||||
|
|
||||||
// Get posts
|
|
||||||
let (posts, _) = Post::fetch(&path, false).await?;
|
|
||||||
|
|
||||||
// Build the RSS feed
|
|
||||||
let channel = ChannelBuilder::default()
|
|
||||||
.title(&subreddit.title)
|
|
||||||
.description(&subreddit.description)
|
|
||||||
.items(
|
|
||||||
posts
|
|
||||||
.into_iter()
|
|
||||||
.map(|post| Item {
|
|
||||||
title: Some(post.title.to_string()),
|
|
||||||
link: Some(utils::get_post_url(&post)),
|
|
||||||
author: Some(post.author.name),
|
|
||||||
content: Some(rewrite_urls(&post.body)),
|
|
||||||
description: Some(format!(
|
|
||||||
"<a href='{}{}'>Comments</a>",
|
|
||||||
config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
|
|
||||||
post.permalink
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Serialize the feed to RSS
|
|
||||||
let body = channel.to_string().into_bytes();
|
|
||||||
|
|
||||||
// Create the HTTP response
|
|
||||||
let mut res = Response::new(Body::from(body));
|
|
||||||
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_fetching_subreddit() {
|
async fn test_fetching_subreddit() {
|
||||||
let subreddit = subreddit("rust", false).await;
|
let subreddit = subreddit("rust", false).await;
|
||||||
assert!(subreddit.is_ok());
|
assert!(subreddit.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn test_gated_and_quarantined() {
|
|
||||||
let quarantined = subreddit("edgy", true).await;
|
|
||||||
assert!(quarantined.is_ok());
|
|
||||||
let gated = subreddit("drugs", true).await;
|
|
||||||
assert!(gated.is_ok());
|
|
||||||
}
|
|
||||||
|
72
src/user.rs
@ -1,12 +1,9 @@
|
|||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
// CRATES
|
// CRATES
|
||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
|
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
|
||||||
use crate::{config, utils};
|
use askama::Template;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use rinja::Template;
|
|
||||||
use time::{macros::format_description, OffsetDateTime};
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -38,8 +35,9 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
// Build the Reddit JSON API path
|
// Build the Reddit JSON API path
|
||||||
let path = format!(
|
let path = format!(
|
||||||
"/user/{}/{listing}.json?{}&raw_json=1",
|
"/user/{}/{}.json?{}&raw_json=1",
|
||||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
||||||
|
listing,
|
||||||
req.uri().query().unwrap_or_default(),
|
req.uri().query().unwrap_or_default(),
|
||||||
);
|
);
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
@ -62,11 +60,11 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
let filters = get_filters(&req);
|
let filters = get_filters(&req);
|
||||||
if filters.contains(&["u_", &username].concat()) {
|
if filters.contains(&["u_", &username].concat()) {
|
||||||
Ok(template(&UserTemplate {
|
template(UserTemplate {
|
||||||
user,
|
user,
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after").unwrap_or_default(), String::new()),
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
listing,
|
listing,
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url,
|
url,
|
||||||
@ -75,7 +73,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered: false,
|
all_posts_filtered: false,
|
||||||
all_posts_hidden_nsfw: false,
|
all_posts_hidden_nsfw: false,
|
||||||
no_posts: false,
|
no_posts: false,
|
||||||
}))
|
})
|
||||||
} else {
|
} else {
|
||||||
// Request user posts/comments from Reddit
|
// Request user posts/comments from Reddit
|
||||||
match Post::fetch(&path, false).await {
|
match Post::fetch(&path, false).await {
|
||||||
@ -83,7 +81,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
|
||||||
let no_posts = posts.is_empty();
|
let no_posts = posts.is_empty();
|
||||||
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
|
||||||
Ok(template(&UserTemplate {
|
template(UserTemplate {
|
||||||
user,
|
user,
|
||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
@ -96,10 +94,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
all_posts_hidden_nsfw,
|
all_posts_hidden_nsfw,
|
||||||
no_posts,
|
no_posts,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
// If there is an error show error page
|
// If there is an error show error page
|
||||||
Err(msg) => error(req, &msg).await,
|
Err(msg) => error(req, msg).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +105,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
// USER
|
// USER
|
||||||
async fn user(name: &str) -> Result<User, String> {
|
async fn user(name: &str) -> Result<User, String> {
|
||||||
// Build the Reddit JSON API path
|
// Build the Reddit JSON API path
|
||||||
let path: String = format!("/user/{name}/about.json?raw_json=1");
|
let path: String = format!("/user/{}/about.json?raw_json=1", name);
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
json(path, false).await.map(|res| {
|
json(path, false).await.map(|res| {
|
||||||
@ -132,56 +130,6 @@ async fn user(name: &str) -> Result<User, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
|
|
||||||
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
|
|
||||||
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
|
|
||||||
}
|
|
||||||
use crate::utils::rewrite_urls;
|
|
||||||
use hyper::header::CONTENT_TYPE;
|
|
||||||
use rss::{ChannelBuilder, Item};
|
|
||||||
|
|
||||||
// Get user
|
|
||||||
let user_str = req.param("name").unwrap_or_default();
|
|
||||||
|
|
||||||
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
|
|
||||||
|
|
||||||
// Get path
|
|
||||||
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);
|
|
||||||
|
|
||||||
// Get user
|
|
||||||
let user_obj = user(&user_str).await.unwrap_or_default();
|
|
||||||
|
|
||||||
// Get posts
|
|
||||||
let (posts, _) = Post::fetch(&path, false).await?;
|
|
||||||
|
|
||||||
// Build the RSS feed
|
|
||||||
let channel = ChannelBuilder::default()
|
|
||||||
.title(user_str)
|
|
||||||
.description(user_obj.description)
|
|
||||||
.items(
|
|
||||||
posts
|
|
||||||
.into_iter()
|
|
||||||
.map(|post| Item {
|
|
||||||
title: Some(post.title.to_string()),
|
|
||||||
link: Some(utils::get_post_url(&post)),
|
|
||||||
author: Some(post.author.name),
|
|
||||||
content: Some(rewrite_urls(&post.body)),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Serialize the feed to RSS
|
|
||||||
let body = channel.to_string().into_bytes();
|
|
||||||
|
|
||||||
// Create the HTTP response
|
|
||||||
let mut res = Response::new(Body::from(body));
|
|
||||||
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_fetching_user() {
|
async fn test_fetching_user() {
|
||||||
let user = user("spez").await;
|
let user = user("spez").await;
|
||||||
|
385
src/utils.rs
@ -1,25 +1,18 @@
|
|||||||
#![allow(dead_code)]
|
use crate::config::get_setting;
|
||||||
#![allow(clippy::cmp_owned)]
|
|
||||||
|
|
||||||
use crate::config::{self, get_setting};
|
|
||||||
//
|
//
|
||||||
// CRATES
|
// CRATES
|
||||||
//
|
//
|
||||||
use crate::{client::json, server::RequestExt};
|
use crate::{client::json, server::RequestExt};
|
||||||
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use log::error;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rinja::Template;
|
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_json_path::{JsonPath, JsonPathExt};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
|
||||||
use time::{macros::format_description, Duration, OffsetDateTime};
|
use time::{macros::format_description, Duration, OffsetDateTime};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@ -49,7 +42,6 @@ pub enum ResourceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post flair with content, background color and foreground color
|
// Post flair with content, background color and foreground color
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Flair {
|
pub struct Flair {
|
||||||
pub flair_parts: Vec<FlairPart>,
|
pub flair_parts: Vec<FlairPart>,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
@ -58,7 +50,7 @@ pub struct Flair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Part of flair, either emoji or text
|
// Part of flair, either emoji or text
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone)]
|
||||||
pub struct FlairPart {
|
pub struct FlairPart {
|
||||||
pub flair_part_type: String,
|
pub flair_part_type: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
@ -100,14 +92,12 @@ impl FlairPart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub flair: Flair,
|
pub flair: Flair,
|
||||||
pub distinguished: String,
|
pub distinguished: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Poll {
|
pub struct Poll {
|
||||||
pub poll_options: Vec<PollOption>,
|
pub poll_options: Vec<PollOption>,
|
||||||
pub voting_end_timestamp: (String, String),
|
pub voting_end_timestamp: (String, String),
|
||||||
@ -125,8 +115,8 @@ impl Poll {
|
|||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
poll_options,
|
poll_options,
|
||||||
voting_end_timestamp,
|
|
||||||
total_vote_count,
|
total_vote_count,
|
||||||
|
voting_end_timestamp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +125,6 @@ impl Poll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct PollOption {
|
pub struct PollOption {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
@ -165,21 +154,18 @@ impl PollOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post flags with nsfw and stickied
|
// Post flags with nsfw and stickied
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
pub spoiler: bool,
|
|
||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
pub stickied: bool,
|
pub stickied: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub alt_url: String,
|
pub alt_url: String,
|
||||||
pub width: i64,
|
pub width: i64,
|
||||||
pub height: i64,
|
pub height: i64,
|
||||||
pub poster: String,
|
pub poster: String,
|
||||||
pub download_name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Media {
|
impl Media {
|
||||||
@ -246,15 +232,6 @@ impl Media {
|
|||||||
|
|
||||||
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
||||||
|
|
||||||
let download_name = if post_type == "image" || post_type == "gif" || post_type == "video" {
|
|
||||||
let permalink_base = url_path_basename(data["permalink"].as_str().unwrap_or_default());
|
|
||||||
let media_url_base = url_path_basename(url_val.as_str().unwrap_or_default());
|
|
||||||
|
|
||||||
format!("redlib_{permalink_base}_{media_url_base}")
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
(
|
||||||
post_type.to_string(),
|
post_type.to_string(),
|
||||||
Self {
|
Self {
|
||||||
@ -265,14 +242,12 @@ impl Media {
|
|||||||
width: source["width"].as_i64().unwrap_or_default(),
|
width: source["width"].as_i64().unwrap_or_default(),
|
||||||
height: source["height"].as_i64().unwrap_or_default(),
|
height: source["height"].as_i64().unwrap_or_default(),
|
||||||
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
poster: format_url(source["url"].as_str().unwrap_or_default()),
|
||||||
download_name,
|
|
||||||
},
|
},
|
||||||
gallery,
|
gallery,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct GalleryMedia {
|
pub struct GalleryMedia {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub width: i64,
|
pub width: i64,
|
||||||
@ -313,7 +288,6 @@ impl GalleryMedia {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post containing content, metadata and media
|
// Post containing content, metadata and media
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -321,7 +295,6 @@ pub struct Post {
|
|||||||
pub body: String,
|
pub body: String,
|
||||||
pub author: Author,
|
pub author: Author,
|
||||||
pub permalink: String,
|
pub permalink: String,
|
||||||
pub link_title: String,
|
|
||||||
pub poll: Option<Poll>,
|
pub poll: Option<Poll>,
|
||||||
pub score: (String, String),
|
pub score: (String, String),
|
||||||
pub upvote_ratio: i64,
|
pub upvote_ratio: i64,
|
||||||
@ -333,13 +306,11 @@ pub struct Post {
|
|||||||
pub domain: String,
|
pub domain: String,
|
||||||
pub rel_time: String,
|
pub rel_time: String,
|
||||||
pub created: String,
|
pub created: String,
|
||||||
pub created_ts: u64,
|
|
||||||
pub num_duplicates: u64,
|
pub num_duplicates: u64,
|
||||||
pub comments: (String, String),
|
pub comments: (String, String),
|
||||||
pub gallery: Vec<GalleryMedia>,
|
pub gallery: Vec<GalleryMedia>,
|
||||||
pub awards: Awards,
|
pub awards: Awards,
|
||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
pub out_url: Option<String>,
|
|
||||||
pub ws_url: String,
|
pub ws_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,8 +326,9 @@ impl Post {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the list of posts from the JSON response
|
// Fetch the list of posts from the JSON response
|
||||||
let Some(post_list) = res["data"]["children"].as_array() else {
|
let post_list = match res["data"]["children"].as_array() {
|
||||||
return Err("No posts found".to_string());
|
Some(list) => list,
|
||||||
|
None => return Err("No posts found".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut posts: Vec<Self> = Vec::new();
|
let mut posts: Vec<Self> = Vec::new();
|
||||||
@ -366,7 +338,6 @@ impl Post {
|
|||||||
let data = &post["data"];
|
let data = &post["data"];
|
||||||
|
|
||||||
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
||||||
let created_ts = data["created_utc"].as_f64().unwrap_or_default().round() as u64;
|
|
||||||
let score = data["score"].as_i64().unwrap_or_default();
|
let score = data["score"].as_i64().unwrap_or_default();
|
||||||
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||||
let title = val(post, "title");
|
let title = val(post, "title");
|
||||||
@ -412,8 +383,7 @@ impl Post {
|
|||||||
alt_url: String::new(),
|
alt_url: String::new(),
|
||||||
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
width: data["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||||
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
height: data["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||||
poster: String::new(),
|
poster: "".to_string(),
|
||||||
download_name: String::new(),
|
|
||||||
},
|
},
|
||||||
media,
|
media,
|
||||||
domain: val(post, "domain"),
|
domain: val(post, "domain"),
|
||||||
@ -432,25 +402,22 @@ impl Post {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: Flags {
|
flags: Flags {
|
||||||
spoiler: data["spoiler"].as_bool().unwrap_or_default(),
|
|
||||||
nsfw: data["over_18"].as_bool().unwrap_or_default(),
|
nsfw: data["over_18"].as_bool().unwrap_or_default(),
|
||||||
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
|
stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(),
|
||||||
},
|
},
|
||||||
permalink: val(post, "permalink"),
|
permalink: val(post, "permalink"),
|
||||||
link_title: val(post, "link_title"),
|
|
||||||
poll: Poll::parse(&data["poll_data"]),
|
poll: Poll::parse(&data["poll_data"]),
|
||||||
rel_time,
|
rel_time,
|
||||||
created,
|
created,
|
||||||
created_ts,
|
|
||||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||||
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
|
comments: format_num(data["num_comments"].as_i64().unwrap_or_default()),
|
||||||
gallery,
|
gallery,
|
||||||
awards,
|
awards,
|
||||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||||
ws_url: val(post, "websocket_url"),
|
ws_url: val(post, "websocket_url"),
|
||||||
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,7 +447,7 @@ pub struct Comment {
|
|||||||
pub prefs: Preferences,
|
pub prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize)]
|
#[derive(Default, Clone)]
|
||||||
pub struct Award {
|
pub struct Award {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub icon_url: String,
|
pub icon_url: String,
|
||||||
@ -489,12 +456,11 @@ pub struct Award {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Award {
|
impl std::fmt::Display for Award {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
write!(f, "{} {} {}", self.name, self.icon_url, self.description)
|
write!(f, "{} {} {}", self.name, self.icon_url, self.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Awards(pub Vec<Award>);
|
pub struct Awards(pub Vec<Award>);
|
||||||
|
|
||||||
impl std::ops::Deref for Awards {
|
impl std::ops::Deref for Awards {
|
||||||
@ -506,8 +472,8 @@ impl std::ops::Deref for Awards {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Awards {
|
impl std::fmt::Display for Awards {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
self.iter().try_fold((), |_, award| writeln!(f, "{award}"))
|
self.iter().fold(Ok(()), |result, award| result.and_then(|_| writeln!(f, "{}", award)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -604,20 +570,14 @@ pub struct Params {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
pub available_themes: Vec<String>,
|
pub available_themes: Vec<String>,
|
||||||
pub available_mascots: Vec<String>,
|
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
pub mascot: String,
|
|
||||||
pub front_page: String,
|
pub front_page: String,
|
||||||
pub layout: String,
|
pub layout: String,
|
||||||
pub wide: String,
|
pub wide: String,
|
||||||
pub blur_spoiler: String,
|
|
||||||
pub show_nsfw: String,
|
pub show_nsfw: String,
|
||||||
pub blur_nsfw: String,
|
pub blur_nsfw: String,
|
||||||
pub hide_hls_notification: String,
|
pub hide_hls_notification: String,
|
||||||
pub video_quality: String,
|
|
||||||
pub hide_sidebar_and_summary: String,
|
|
||||||
pub use_hls: String,
|
pub use_hls: String,
|
||||||
pub ffmpeg_video_downloads: String,
|
|
||||||
pub autoplay_videos: String,
|
pub autoplay_videos: String,
|
||||||
pub fixed_navbar: String,
|
pub fixed_navbar: String,
|
||||||
pub disable_visit_reddit_confirmation: String,
|
pub disable_visit_reddit_confirmation: String,
|
||||||
@ -634,11 +594,6 @@ pub struct Preferences {
|
|||||||
#[include = "*.css"]
|
#[include = "*.css"]
|
||||||
pub struct ThemeAssets;
|
pub struct ThemeAssets;
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "static/mascots/"]
|
|
||||||
#[include = "*.png"]
|
|
||||||
pub struct MascotAssets;
|
|
||||||
|
|
||||||
impl Preferences {
|
impl Preferences {
|
||||||
// Build preferences from cookies
|
// Build preferences from cookies
|
||||||
pub fn new(req: &Request<Body>) -> Self {
|
pub fn new(req: &Request<Body>) -> Self {
|
||||||
@ -647,31 +602,18 @@ impl Preferences {
|
|||||||
let mut themes = vec!["system".to_string()];
|
let mut themes = vec!["system".to_string()];
|
||||||
for file in ThemeAssets::iter() {
|
for file in ThemeAssets::iter() {
|
||||||
let chunks: Vec<&str> = file.as_ref().split(".css").collect();
|
let chunks: Vec<&str> = file.as_ref().split(".css").collect();
|
||||||
themes.push(chunks[0].to_owned());
|
themes.push(chunks[0].to_owned())
|
||||||
}
|
|
||||||
// Read available mascot names from embedded png files.
|
|
||||||
// Always make default "none" option available.
|
|
||||||
let mut mascots = vec!["none".to_string()];
|
|
||||||
for file in MascotAssets::iter() {
|
|
||||||
let chunks: Vec<&str> = file.as_ref().split(".png").collect();
|
|
||||||
mascots.push(chunks[0].to_owned());
|
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
available_themes: themes,
|
available_themes: themes,
|
||||||
available_mascots: mascots,
|
|
||||||
theme: setting(req, "theme"),
|
theme: setting(req, "theme"),
|
||||||
mascot: setting(req, "mascot"),
|
|
||||||
front_page: setting(req, "front_page"),
|
front_page: setting(req, "front_page"),
|
||||||
layout: setting(req, "layout"),
|
layout: setting(req, "layout"),
|
||||||
wide: setting(req, "wide"),
|
wide: setting(req, "wide"),
|
||||||
blur_spoiler: setting(req, "blur_spoiler"),
|
|
||||||
show_nsfw: setting(req, "show_nsfw"),
|
show_nsfw: setting(req, "show_nsfw"),
|
||||||
hide_sidebar_and_summary: setting(req, "hide_sidebar_and_summary"),
|
|
||||||
blur_nsfw: setting(req, "blur_nsfw"),
|
blur_nsfw: setting(req, "blur_nsfw"),
|
||||||
use_hls: setting(req, "use_hls"),
|
use_hls: setting(req, "use_hls"),
|
||||||
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
|
|
||||||
hide_hls_notification: setting(req, "hide_hls_notification"),
|
hide_hls_notification: setting(req, "hide_hls_notification"),
|
||||||
video_quality: setting(req, "video_quality"),
|
|
||||||
autoplay_videos: setting(req, "autoplay_videos"),
|
autoplay_videos: setting(req, "autoplay_videos"),
|
||||||
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
|
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
|
||||||
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
|
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
|
||||||
@ -714,7 +656,7 @@ pub fn filter_posts(posts: &mut Vec<Post>, filters: &HashSet<String>) -> (u64, b
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [`Post`] from a provided JSON.
|
/// Creates a [`Post`] from a provided JSON.
|
||||||
pub async fn parse_post(post: &Value) -> Post {
|
pub async fn parse_post(post: &serde_json::Value) -> Post {
|
||||||
// Grab UTC time as unix timestamp
|
// Grab UTC time as unix timestamp
|
||||||
let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default());
|
let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default());
|
||||||
// Parse post score and upvote ratio
|
// Parse post score and upvote ratio
|
||||||
@ -724,8 +666,6 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
// Determine the type of media along with the media URL
|
// Determine the type of media along with the media URL
|
||||||
let (post_type, media, gallery) = Media::parse(&post["data"]).await;
|
let (post_type, media, gallery) = Media::parse(&post["data"]).await;
|
||||||
|
|
||||||
let created_ts = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as u64;
|
|
||||||
|
|
||||||
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
|
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
|
||||||
|
|
||||||
let permalink = val(post, "permalink");
|
let permalink = val(post, "permalink");
|
||||||
@ -734,8 +674,9 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
|
|
||||||
let body = if val(post, "removed_by_category") == "moderator" {
|
let body = if val(post, "removed_by_category") == "moderator" {
|
||||||
format!(
|
format!(
|
||||||
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{permalink}\">view removed post</a></p></div>",
|
"<div class=\"md\"><p>[removed] — <a href=\"https://{}{}\">view removed post</a></p></div>",
|
||||||
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or(String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)),
|
||||||
|
permalink
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
rewrite_urls(&val(post, "selftext_html"))
|
rewrite_urls(&val(post, "selftext_html"))
|
||||||
@ -762,7 +703,6 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
distinguished: val(post, "distinguished"),
|
distinguished: val(post, "distinguished"),
|
||||||
},
|
},
|
||||||
permalink,
|
permalink,
|
||||||
link_title: val(post, "link_title"),
|
|
||||||
poll,
|
poll,
|
||||||
score: format_num(score),
|
score: format_num(score),
|
||||||
upvote_ratio: ratio as i64,
|
upvote_ratio: ratio as i64,
|
||||||
@ -774,7 +714,6 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
|
width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(),
|
||||||
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(),
|
||||||
poster: String::new(),
|
poster: String::new(),
|
||||||
download_name: String::new(),
|
|
||||||
},
|
},
|
||||||
flair: Flair {
|
flair: Flair {
|
||||||
flair_parts: FlairPart::parse(
|
flair_parts: FlairPart::parse(
|
||||||
@ -791,21 +730,18 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: Flags {
|
flags: Flags {
|
||||||
spoiler: post["data"]["spoiler"].as_bool().unwrap_or_default(),
|
|
||||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||||
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
|
stickied: post["data"]["stickied"].as_bool().unwrap_or_default() || post["data"]["pinned"].as_bool().unwrap_or(false),
|
||||||
},
|
},
|
||||||
domain: val(post, "domain"),
|
domain: val(post, "domain"),
|
||||||
rel_time,
|
rel_time,
|
||||||
created,
|
created,
|
||||||
created_ts,
|
|
||||||
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0),
|
||||||
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
|
comments: format_num(post["data"]["num_comments"].as_i64().unwrap_or_default()),
|
||||||
gallery,
|
gallery,
|
||||||
awards,
|
awards,
|
||||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||||
ws_url: val(post, "websocket_url"),
|
ws_url: val(post, "websocket_url"),
|
||||||
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,7 +752,7 @@ pub async fn parse_post(post: &Value) -> Post {
|
|||||||
// Grab a query parameter from a url
|
// Grab a query parameter from a url
|
||||||
pub fn param(path: &str, value: &str) -> Option<String> {
|
pub fn param(path: &str, value: &str) -> Option<String> {
|
||||||
Some(
|
Some(
|
||||||
Url::parse(format!("https://libredd.it/{path}").as_str())
|
Url::parse(format!("https://libredd.it/{}", path).as_str())
|
||||||
.ok()?
|
.ok()?
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
.into_owned()
|
.into_owned()
|
||||||
@ -833,7 +769,7 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
|
|||||||
.cookie(name)
|
.cookie(name)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// If there is no cookie for this setting, try receiving a default from the config
|
// If there is no cookie for this setting, try receiving a default from the config
|
||||||
if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) {
|
if let Some(default) = crate::config::get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) {
|
||||||
Cookie::new(name, default)
|
Cookie::new(name, default)
|
||||||
} else {
|
} else {
|
||||||
Cookie::from(name)
|
Cookie::from(name)
|
||||||
@ -846,21 +782,21 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
|
|||||||
// Retrieve the value of a setting by name or the default value
|
// Retrieve the value of a setting by name or the default value
|
||||||
pub fn setting_or_default(req: &Request<Body>, name: &str, default: String) -> String {
|
pub fn setting_or_default(req: &Request<Body>, name: &str, default: String) -> String {
|
||||||
let value = setting(req, name);
|
let value = setting(req, name);
|
||||||
if value.is_empty() {
|
if !value.is_empty() {
|
||||||
default
|
|
||||||
} else {
|
|
||||||
value
|
value
|
||||||
|
} else {
|
||||||
|
default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect and redirect in the event of a random subreddit
|
// Detect and redirect in the event of a random subreddit
|
||||||
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
||||||
if sub == "random" || sub == "randnsfw" {
|
if sub == "random" || sub == "randnsfw" {
|
||||||
let new_sub = json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"]
|
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
Ok(redirect(&format!("/r/{new_sub}{additional}")))
|
Ok(redirect(format!("/r/{}{}", new_sub, additional)))
|
||||||
} else {
|
} else {
|
||||||
Err("No redirect needed".to_string())
|
Err("No redirect needed".to_string())
|
||||||
}
|
}
|
||||||
@ -940,162 +876,32 @@ pub fn format_url(url: &str) -> String {
|
|||||||
|
|
||||||
// These are links we want to replace in-body
|
// These are links we want to replace in-body
|
||||||
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap());
|
static REDDIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|new\.|)(reddit\.com|redd\.it)/"#).unwrap());
|
||||||
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview|i)\.redd\.it(.*)[^?]").unwrap());
|
static REDDIT_PREVIEW_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(external-preview|preview)\.redd\.it(.*)[^?]").unwrap());
|
||||||
static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap());
|
static REDDIT_EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"https?://(www|).redditstatic\.com/(.*)").unwrap());
|
||||||
static REDLIB_PREVIEW_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"/(img|preview/)(pre|external-pre)?/(.*?)>"#).unwrap());
|
|
||||||
static REDLIB_PREVIEW_TEXT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r">(.*?)</a>").unwrap());
|
|
||||||
|
|
||||||
// Rewrite Reddit links to Redlib in body of text
|
// Rewrite Reddit links to Redlib in body of text
|
||||||
pub fn rewrite_urls(input_text: &str) -> String {
|
pub fn rewrite_urls(input_text: &str) -> String {
|
||||||
let mut text1 =
|
let text1 =
|
||||||
// Rewrite Reddit links to Redlib
|
// Rewrite Reddit links to Redlib
|
||||||
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
|
REDDIT_REGEX.replace_all(input_text, r#"href="/"#)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let text1 = REDDIT_EMOJI_REGEX
|
||||||
loop {
|
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||||
if REDDIT_EMOJI_REGEX.find(&text1).is_none() {
|
.to_string()
|
||||||
break;
|
// Remove (html-encoded) "\" from URLs.
|
||||||
} else {
|
.replace("%5C", "")
|
||||||
text1 = REDDIT_EMOJI_REGEX
|
.replace("\\_", "_");
|
||||||
.replace_all(&text1, format_url(REDDIT_EMOJI_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove (html-encoded) "\" from URLs.
|
|
||||||
text1 = text1.replace("%5C", "").replace("\\_", "_");
|
|
||||||
|
|
||||||
// Rewrite external media previews to Redlib
|
// Rewrite external media previews to Redlib
|
||||||
loop {
|
if REDDIT_PREVIEW_REGEX.is_match(&text1) {
|
||||||
if REDDIT_PREVIEW_REGEX.find(&text1).is_none() {
|
REDDIT_PREVIEW_REGEX
|
||||||
return text1;
|
.replace_all(&text1, format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default()))
|
||||||
} else {
|
.to_string()
|
||||||
let formatted_url = format_url(REDDIT_PREVIEW_REGEX.find(&text1).map(|x| x.as_str()).unwrap_or_default());
|
} else {
|
||||||
|
text1
|
||||||
let image_url = REDLIB_PREVIEW_LINK_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
|
|
||||||
let mut image_caption = REDLIB_PREVIEW_TEXT_REGEX.find(&formatted_url).map_or("", |m| m.as_str()).to_string();
|
|
||||||
|
|
||||||
/* As long as image_caption isn't empty remove first and last four characters of image_text to leave us with just the text in the caption without any HTML.
|
|
||||||
This makes it possible to enclose it in a <figcaption> later on without having stray HTML breaking it */
|
|
||||||
if !image_caption.is_empty() {
|
|
||||||
image_caption = image_caption[1..image_caption.len() - 4].to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
// image_url contains > at the end of it, and right above this we remove image_text's front >, leaving us with just a single > between them
|
|
||||||
let image_to_replace = format!("<a href=\"{image_url}{image_caption}</a>");
|
|
||||||
|
|
||||||
// _image_replacement needs to be in scope for the replacement at the bottom of the loop
|
|
||||||
let mut _image_replacement = String::new();
|
|
||||||
|
|
||||||
/* We don't want to show a caption that's just the image's link, so we check if we find a Reddit preview link within the image's caption.
|
|
||||||
If we don't find one we must have actual text, so we include a <figcaption> block that contains it.
|
|
||||||
Otherwise we don't include the <figcaption> block as we don't need it. */
|
|
||||||
if REDDIT_PREVIEW_REGEX.find(&image_caption).is_none() {
|
|
||||||
// Without this " would show as \" instead. "\"" is how the quotes are formatted within image_text beforehand
|
|
||||||
image_caption = image_caption.replace("\\"", "\"");
|
|
||||||
|
|
||||||
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a><figcaption>{image_caption}</figcaption></figure>");
|
|
||||||
} else {
|
|
||||||
_image_replacement = format!("<figure><a href=\"{image_url}<img loading=\"lazy\" src=\"{image_url}</a></figure>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* In order to know if we're dealing with a normal or external preview we need to take a look at the first capture group of REDDIT_PREVIEW_REGEX
|
|
||||||
if it's preview we're dealing with something that needs /preview/pre, external-preview is /preview/external-pre, and i is /img */
|
|
||||||
let reddit_preview_regex_capture = REDDIT_PREVIEW_REGEX.captures(&text1).unwrap().get(1).map_or("", |m| m.as_str()).to_string();
|
|
||||||
let mut _preview_type = String::new();
|
|
||||||
if reddit_preview_regex_capture == "preview" {
|
|
||||||
_preview_type = "/preview/pre".to_string();
|
|
||||||
} else if reddit_preview_regex_capture == "external-preview" {
|
|
||||||
_preview_type = "/preview/external-pre".to_string();
|
|
||||||
} else {
|
|
||||||
_preview_type = "/img".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
text1 = REDDIT_PREVIEW_REGEX
|
|
||||||
.replace(&text1, format!("{_preview_type}$2"))
|
|
||||||
.replace(&image_to_replace, &_image_replacement)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png"
|
|
||||||
static REDDIT_EMOTE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap());
|
|
||||||
|
|
||||||
// These all follow a pattern of '"emote|SUBREDDIT_IT|NUMBER"', we want the number
|
|
||||||
static REDDIT_EMOTE_ID_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#""emote\|.*\|(.*)""#).unwrap());
|
|
||||||
|
|
||||||
pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String {
|
|
||||||
/* Create the paths we'll use to look for our data inside the json.
|
|
||||||
Because we don't know the name of any given emote we use a wildcard to parse them. */
|
|
||||||
let link_path = JsonPath::parse("$[*].s.u").expect("valid JSON Path");
|
|
||||||
let id_path = JsonPath::parse("$[*].id").expect("valid JSON Path");
|
|
||||||
let size_path = JsonPath::parse("$[*].s.y").expect("valid JSON Path");
|
|
||||||
|
|
||||||
// Extract all of the results from those json paths
|
|
||||||
let link_nodes = media_metadata.json_path(&link_path);
|
|
||||||
let id_nodes = media_metadata.json_path(&id_path);
|
|
||||||
|
|
||||||
// Initialize our vectors
|
|
||||||
let mut id_vec = Vec::new();
|
|
||||||
let mut link_vec = Vec::new();
|
|
||||||
|
|
||||||
// Add the relevant data to each of our vectors so we can access it by number later
|
|
||||||
for current_id in id_nodes {
|
|
||||||
id_vec.push(current_id)
|
|
||||||
}
|
|
||||||
for current_link in link_nodes {
|
|
||||||
link_vec.push(current_link)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set index to the length of link_vec.
|
|
||||||
This is one larger than we'll actually be looking at, but we correct that later */
|
|
||||||
let mut index = link_vec.len();
|
|
||||||
|
|
||||||
// Comment needs to be in scope for when we call rewrite_urls()
|
|
||||||
let mut comment = comment;
|
|
||||||
|
|
||||||
/* Loop until index hits zero.
|
|
||||||
This also prevents us from trying to do anything on an empty vector */
|
|
||||||
while index != 0 {
|
|
||||||
/* Subtract 1 from index to get the real index we should be looking at.
|
|
||||||
Then continue on each subsequent loop to continue until we hit the last entry in the vector.
|
|
||||||
This is how we get this to deal with multiple emotes in a single message and properly replace each ID with it's link */
|
|
||||||
index -= 1;
|
|
||||||
|
|
||||||
// Convert our current index in id_vec into a string so we can search through it with regex
|
|
||||||
let current_id = id_vec[index].to_string();
|
|
||||||
|
|
||||||
/* The ID number can be multiple lengths, so we capture it with regex.
|
|
||||||
We also want to only attempt anything when we get matches to avoid panicking */
|
|
||||||
if let Some(id_capture) = REDDIT_EMOTE_ID_NUMBER_REGEX.captures(¤t_id) {
|
|
||||||
// Format the ID to include the colons it has in the comment text
|
|
||||||
let id = format!(":{}:", &id_capture[1]);
|
|
||||||
|
|
||||||
// Convert current link to string to search through it with the regex
|
|
||||||
let link = link_vec[index].to_string();
|
|
||||||
|
|
||||||
// Make sure we only do operations when we get matches, otherwise we panic when trying to access the first match
|
|
||||||
if let Some(link_capture) = REDDIT_EMOTE_LINK_REGEX.captures(&link) {
|
|
||||||
/* Reddit sends a size for the image based on whether it's alone or accompanied by text.
|
|
||||||
It's a good idea and makes everything look nicer, so we'll do the same. */
|
|
||||||
let size = media_metadata.json_path(&size_path).first().unwrap().to_string();
|
|
||||||
|
|
||||||
// Replace the ID we found earlier in the comment with the respective image and it's link from the regex capture
|
|
||||||
let to_replace_with = format!(
|
|
||||||
"<img loading=\"lazy\" src=\"/emote/{} width=\"{size}\" height=\"{size}\" style=\"vertical-align:text-bottom\">",
|
|
||||||
&link_capture[1]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Inside the comment replace the ID we found with the string that will embed the image
|
|
||||||
comment = comment.replace(&id, &to_replace_with).to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Call rewrite_urls() to transform any other Reddit links
|
|
||||||
rewrite_urls(&comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format vote count to a string that will be displayed.
|
// Format vote count to a string that will be displayed.
|
||||||
// Append `m` and `k` for millions and thousands respectively, and
|
// Append `m` and `k` for millions and thousands respectively, and
|
||||||
// round to the nearest tenth.
|
// round to the nearest tenth.
|
||||||
@ -1156,26 +962,27 @@ pub fn val(j: &Value, k: &str) -> String {
|
|||||||
// NETWORKING
|
// NETWORKING
|
||||||
//
|
//
|
||||||
|
|
||||||
pub fn template(t: &impl Template) -> Response<Body> {
|
pub fn template(t: impl Template) -> Result<Response<Body>, String> {
|
||||||
Response::builder()
|
Ok(
|
||||||
.status(200)
|
Response::builder()
|
||||||
.header("content-type", "text/html")
|
.status(200)
|
||||||
.body(t.render().unwrap_or_default().into())
|
.header("content-type", "text/html")
|
||||||
.unwrap_or_default()
|
.body(t.render().unwrap_or_default().into())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redirect(path: &str) -> Response<Body> {
|
pub fn redirect(path: String) -> Response<Body> {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(302)
|
.status(302)
|
||||||
.header("content-type", "text/html")
|
.header("content-type", "text/html")
|
||||||
.header("Location", path)
|
.header("Location", &path)
|
||||||
.body(format!("Redirecting to <a href=\"{path}\">{path}</a>...").into())
|
.body(format!("Redirecting to <a href=\"{0}\">{0}</a>...", path).into())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a generic error landing page.
|
/// Renders a generic error landing page.
|
||||||
pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, String> {
|
pub async fn error(req: Request<Body>, msg: impl ToString) -> Result<Response<Body>, String> {
|
||||||
error!("Error page rendered: {}", msg.split('|').next().unwrap_or_default());
|
|
||||||
let url = req.uri().to_string();
|
let url = req.uri().to_string();
|
||||||
let body = ErrorTemplate {
|
let body = ErrorTemplate {
|
||||||
msg: msg.to_string(),
|
msg: msg.to_string(),
|
||||||
@ -1196,29 +1003,7 @@ pub async fn error(req: Request<Body>, msg: &str) -> Result<Response<Body>, Stri
|
|||||||
/// subreddits or posts or userpages for users Reddit has deemed NSFW will
|
/// subreddits or posts or userpages for users Reddit has deemed NSFW will
|
||||||
/// be denied.
|
/// be denied.
|
||||||
pub fn sfw_only() -> bool {
|
pub fn sfw_only() -> bool {
|
||||||
match get_setting("REDLIB_SFW_ONLY") {
|
match crate::config::get_setting("REDLIB_SFW_ONLY") {
|
||||||
Some(val) => val == "on",
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
|
|
||||||
/// If this variable is set as such, the instance will enable RSS feeds.
|
|
||||||
/// Otherwise, the instance will not provide RSS feeds.
|
|
||||||
pub fn enable_rss() -> bool {
|
|
||||||
match get_setting("REDLIB_ENABLE_RSS") {
|
|
||||||
Some(val) => val == "on",
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the config/env variable `REDLIB_ROBOTS_DISABLE_INDEXING` carries the
|
|
||||||
/// value `on`.
|
|
||||||
///
|
|
||||||
/// If this variable is set as such, the instance will block all robots in robots.txt and
|
|
||||||
/// insert the noindex, nofollow meta tag on every page.
|
|
||||||
pub fn disable_indexing() -> bool {
|
|
||||||
match get_setting("REDLIB_ROBOTS_DISABLE_INDEXING") {
|
|
||||||
Some(val) => val == "on",
|
Some(val) => val == "on",
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
@ -1242,7 +1027,7 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
|
|||||||
|
|
||||||
// Determine from the request URL if the resource is a subreddit, a user
|
// Determine from the request URL if the resource is a subreddit, a user
|
||||||
// page, or a post.
|
// page, or a post.
|
||||||
let resource: String = if !req.param("name").unwrap_or_default().is_empty() {
|
let res: String = if !req.param("name").unwrap_or_default().is_empty() {
|
||||||
res_type = ResourceType::User;
|
res_type = ResourceType::User;
|
||||||
req.param("name").unwrap_or_default()
|
req.param("name").unwrap_or_default()
|
||||||
} else if !req.param("id").unwrap_or_default().is_empty() {
|
} else if !req.param("id").unwrap_or_default().is_empty() {
|
||||||
@ -1254,7 +1039,7 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
|
|||||||
};
|
};
|
||||||
|
|
||||||
let body = NSFWLandingTemplate {
|
let body = NSFWLandingTemplate {
|
||||||
res: resource,
|
res,
|
||||||
res_type,
|
res_type,
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
url: req_url,
|
url: req_url,
|
||||||
@ -1265,34 +1050,6 @@ pub async fn nsfw_landing(req: Request<Body>, req_url: String) -> Result<Respons
|
|||||||
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
|
Ok(Response::builder().status(403).header("content-type", "text/html").body(body.into()).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the last (non-empty) segment of a path string
|
|
||||||
pub fn url_path_basename(path: &str) -> String {
|
|
||||||
let url_result = Url::parse(format!("https://libredd.it/{path}").as_str());
|
|
||||||
|
|
||||||
if url_result.is_err() {
|
|
||||||
path.to_string()
|
|
||||||
} else {
|
|
||||||
let mut url = url_result.unwrap();
|
|
||||||
url.path_segments_mut().unwrap().pop_if_empty();
|
|
||||||
|
|
||||||
url.path_segments().unwrap().last().unwrap().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the URL of a post, as needed by RSS feeds
|
|
||||||
pub fn get_post_url(post: &Post) -> String {
|
|
||||||
if let Some(out_url) = &post.out_url {
|
|
||||||
// Handle cross post
|
|
||||||
if out_url.starts_with("/r/") {
|
|
||||||
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
|
|
||||||
} else {
|
|
||||||
out_url.to_string()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{format_num, format_url, rewrite_urls};
|
use super::{format_num, format_url, rewrite_urls};
|
||||||
@ -1393,35 +1150,3 @@ async fn test_fetching_ws() {
|
|||||||
assert!(post.ws_url.starts_with("wss://k8s-lb.wss.redditmedia.com/link/"));
|
assert!(post.ws_url.starts_with("wss://k8s-lb.wss.redditmedia.com/link/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rewriting_image_links() {
|
|
||||||
let input =
|
|
||||||
r#"<p><a href="https://preview.redd.it/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc">caption 1</a></p>"#;
|
|
||||||
let output = r#"<p><figure><a href="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"><img loading="lazy" src="/preview/pre/6awags382xo31.png?width=2560&format=png&auto=webp&s=9c563aed4f07a91bdd249b5a3cea43a79710dcfc"></a><figcaption>caption 1</figcaption></figure></p"#;
|
|
||||||
assert_eq!(rewrite_urls(input), output);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_url_path_basename() {
|
|
||||||
// without trailing slash
|
|
||||||
assert_eq!(url_path_basename("/first/last"), "last");
|
|
||||||
// with trailing slash
|
|
||||||
assert_eq!(url_path_basename("/first/last/"), "last");
|
|
||||||
// with query parameters
|
|
||||||
assert_eq!(url_path_basename("/first/last/?some=query"), "last");
|
|
||||||
// file path
|
|
||||||
assert_eq!(url_path_basename("/cdn/image.jpg"), "image.jpg");
|
|
||||||
// when a full url is passed instead of just a path
|
|
||||||
assert_eq!(url_path_basename("https://doma.in/first/last"), "last");
|
|
||||||
// empty path
|
|
||||||
assert_eq!(url_path_basename("/"), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rewriting_emotes() {
|
|
||||||
let json_input = serde_json::from_str(r#"{"emote|t5_31hpy|2028":{"e":"Image","id":"emote|t5_31hpy|2028","m":"image/png","s":{"u":"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/t5_31hpy/PW6WsOaLcd.png","x":60,"y":60},"status":"valid","t":"sticker"}}"#).expect("Valid JSON");
|
|
||||||
let comment_input = r#"<div class="comment_body "><div class="md"><p>:2028:</p></div></div>"#;
|
|
||||||
let output = r#"<div class="comment_body "><div class="md"><p><img loading="lazy" src="/emote/t5_31hpy/PW6WsOaLcd.png" width="60" height="60" style="vertical-align:text-bottom"></p></div></div>"#;
|
|
||||||
assert_eq!(rewrite_emotes(&json_input, comment_input.to_string()), output);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 8.0 KiB |
@ -1,55 +0,0 @@
|
|||||||
async function checkInstanceUpdateStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/commits.json');
|
|
||||||
const text = await response.text();
|
|
||||||
const entries = JSON.parse(text);
|
|
||||||
const localCommit = document.getElementById('git_commit').dataset.value;
|
|
||||||
|
|
||||||
let statusMessage = '';
|
|
||||||
|
|
||||||
if (entries.length > 0) {
|
|
||||||
const commitHashes = Array.from(entries).map(entry => {
|
|
||||||
return entry.sha
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitIndex = commitHashes.indexOf(localCommit);
|
|
||||||
|
|
||||||
if (commitIndex === 0) {
|
|
||||||
statusMessage = '✅ Instance is up to date.';
|
|
||||||
} else if (commitIndex > 0) {
|
|
||||||
statusMessage = `⚠️ This instance is not up to date and is ${commitIndex} commits old. Test and confirm on an up-to-date instance before reporting.`;
|
|
||||||
document.getElementById('error-318').remove();
|
|
||||||
} else {
|
|
||||||
statusMessage = `⚠️ This instance is not up to date and is at least ${commitHashes.length} commits old. Test and confirm on an up-to-date instance before reporting.`;
|
|
||||||
document.getElementById('error-318').remove();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusMessage = '⚠️ Unable to fetch commit information.';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('update-status').innerText = statusMessage;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching commits:', error);
|
|
||||||
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkOtherInstances() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/instances.json');
|
|
||||||
const data = await response.json();
|
|
||||||
const randomInstance = data.instances[Math.floor(Math.random() * data.instances.length)];
|
|
||||||
const instanceUrl = randomInstance.url;
|
|
||||||
// Set the href of the <a> tag to the instance URL with path included
|
|
||||||
document.getElementById('random-instance').href = instanceUrl + window.location.pathname;
|
|
||||||
//document.getElementById('random-instance').innerText = "Visit Random Instance";
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching instances:', error);
|
|
||||||
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the target URL when the page loads
|
|
||||||
window.addEventListener('load', checkOtherInstances);
|
|
||||||
|
|
||||||
checkInstanceUpdateStatus();
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 969 B |
@ -1,2 +0,0 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(a){var o=t[a];if(void 0!==o)return o.exports;var s=t[a]={exports:{}};return e[a](s,s.exports,r),s.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(e||(e={}));const t=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),o=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let s;self.onmessage=async({data:{id:n,type:E,data:i}})=>{const c=[];let p;try{if(E!==e.LOAD&&!s)throw a;switch(E){case e.LOAD:p=await(async({coreURL:t="https://unpkg.com/@ffmpeg/core@0.12.1/dist/umd/ffmpeg-core.js",wasmURL:a,workerURL:n})=>{const E=!s,i=t,c=a||t.replace(/.js$/g,".wasm"),p=n||t.replace(/.js$/g,".worker.js");try{importScripts(i)}catch{if(self.createFFmpegCore=(await r(454)(i)).default,!self.createFFmpegCore)throw o}return s=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:c,workerURL:p}))}`}),s.setLogger((t=>self.postMessage({type:e.LOG,data:t}))),s.setProgress((t=>self.postMessage({type:e.PROGRESS,data:t}))),E})(i);break;case e.EXEC:p=(({args:e,timeout:t=-1})=>{s.setTimeout(t),s.exec(...e);const r=s.ret;return s.reset(),r})(i);break;case e.WRITE_FILE:p=(({path:e,data:t})=>(s.FS.writeFile(e,t),!0))(i);break;case e.READ_FILE:p=(({path:e,encoding:t})=>s.FS.readFile(e,{encoding:t}))(i);break;case e.DELETE_FILE:p=(({path:e})=>(s.FS.unlink(e),!0))(i);break;case e.RENAME:p=(({oldPath:e,newPath:t})=>(s.FS.rename(e,t),!0))(i);break;case e.CREATE_DIR:p=(({path:e})=>(s.FS.mkdir(e),!0))(i);break;case e.LIST_DIR:p=(({path:e})=>{const t=s.FS.readdir(e),r=[];for(const a of t){const t=s.FS.stat(`${e}/${a}`),o=s.FS.isDir(t.mode);r.push({name:a,isDir:o})}return r})(i);break;case e.DELETE_DIR:p=(({path:e})=>(s.FS.rmdir(e),!0))(i);break;case e.MOUNT:p=(({fsType:e,options:t,mountPoint:r})=>{let a=e,o=s.FS.filesystems[a];return!!o&&(s.FS.mount(o,t,r),!0)})(i);break;case e.UNMOUNT:p=(({mountPoint:e})=>(s.FS.unmount(e),!0))(i);break;default:throw t}}catch(t){return void self.postMessage({id:n,type:e.ERROR,data:t.toString()})}p instanceof Uint8Array&&c.push(p.buffer),self.postMessage({id:n,type:E,data:p},c)}})(),{}})()));
|
|
||||||
//# sourceMappingURL=814.ffmpeg.js.map
|
|
@ -1 +0,0 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegUtil=t():e.FFmpegUtil=t()}(self,(()=>(()=>{"use strict";var e={591:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.HeaderContentLength=void 0,t.HeaderContentLength="Content-Length"},431:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ERROR_INCOMPLETED_DOWNLOAD=t.ERROR_RESPONSE_BODY_READER=void 0,t.ERROR_RESPONSE_BODY_READER=new Error("failed to get response body reader"),t.ERROR_INCOMPLETED_DOWNLOAD=new Error("failed to complete download")},915:function(e,t,o){var r=this&&this.__awaiter||function(e,t,o,r){return new(o||(o=Promise))((function(n,i){function d(e){try{l(r.next(e))}catch(e){i(e)}}function a(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?n(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(d,a)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.toBlobURL=t.downloadWithProgress=t.importScript=t.fetchFile=void 0;const n=o(431),i=o(591);t.fetchFile=e=>r(void 0,void 0,void 0,(function*(){let t;if("string"==typeof e)t=/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?atob(e.split(",")[1]).split("").map((e=>e.charCodeAt(0))):yield(yield fetch(e)).arrayBuffer();else if(e instanceof URL)t=yield(yield fetch(e)).arrayBuffer();else{if(!(e instanceof File||e instanceof Blob))return new Uint8Array;t=yield(o=e,new Promise(((e,t)=>{const r=new FileReader;r.onload=()=>{const{result:t}=r;t instanceof ArrayBuffer?e(new Uint8Array(t)):e(new Uint8Array)},r.onerror=e=>{var o,r;t(Error(`File could not be read! Code=${(null===(r=null===(o=null==e?void 0:e.target)||void 0===o?void 0:o.error)||void 0===r?void 0:r.code)||-1}`))},r.readAsArrayBuffer(o)})))}var o;return new Uint8Array(t)})),t.importScript=e=>r(void 0,void 0,void 0,(function*(){return new Promise((t=>{const o=document.createElement("script"),r=()=>{o.removeEventListener("load",r),t()};o.src=e,o.type="text/javascript",o.addEventListener("load",r),document.getElementsByTagName("head")[0].appendChild(o)}))})),t.downloadWithProgress=(e,t)=>r(void 0,void 0,void 0,(function*(){var o;const r=yield fetch(e);let d;try{const a=parseInt(r.headers.get(i.HeaderContentLength)||"-1"),l=null===(o=r.body)||void 0===o?void 0:o.getReader();if(!l)throw n.ERROR_RESPONSE_BODY_READER;const c=[];let s=0;for(;;){const{done:o,value:r}=yield l.read(),i=r?r.length:0;if(o){if(-1!=a&&a!==s)throw n.ERROR_INCOMPLETED_DOWNLOAD;t&&t({url:e,total:a,received:s,delta:i,done:o});break}c.push(r),s+=i,t&&t({url:e,total:a,received:s,delta:i,done:o})}const f=new Uint8Array(s);let u=0;for(const e of c)f.set(e,u),u+=e.length;d=f.buffer}catch(o){console.log("failed to send download progress event: ",o),d=yield r.arrayBuffer(),t&&t({url:e,total:d.byteLength,received:d.byteLength,delta:0,done:!0})}return d})),t.toBlobURL=(e,o,n=!1,i)=>r(void 0,void 0,void 0,(function*(){const r=n?yield(0,t.downloadWithProgress)(e,i):yield(yield fetch(e)).arrayBuffer(),d=new Blob([r],{type:o});return URL.createObjectURL(d)}))}},t={};return function o(r){var n=t[r];if(void 0!==n)return n.exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,o),i.exports}(915)})()));
|
|
@ -1,2 +0,0 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=(s={},{signal:r}={})=>(this.#e||(this.#e=new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:s},void 0,r));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}return s})()));
|
|
||||||
//# sourceMappingURL=ffmpeg.js.map
|
|
6
static/hls.min.js
vendored
BIN
static/logo.png
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 7.9 KiB |
@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
id="svg2"
|
|
||||||
width="512"
|
|
||||||
height="512"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs id="defs2" />
|
|
||||||
<rect width="512" height="512" fill="#4c082a" />
|
|
||||||
<g
|
|
||||||
transform="matrix(0.75272,0,0,0.75272,-1.1596187,-0.37987125)"
|
|
||||||
id="g2">
|
|
||||||
<circle
|
|
||||||
fill="#1a1a1a"
|
|
||||||
id="circle1"
|
|
||||||
style="fill:#4c082a;fill-opacity:0"
|
|
||||||
r="340.10001"
|
|
||||||
cy="340.32001"
|
|
||||||
cx="341.10999" />
|
|
||||||
<path
|
|
||||||
d="m 320.64,126.73 v 300.8 h 92.264 V 219.61 h 75.803 v -92.83 h -75.803 v -0.0508 z"
|
|
||||||
fill="#f83240"
|
|
||||||
id="path1"
|
|
||||||
style="fill:#f83240;fill-opacity:1" />
|
|
||||||
<path
|
|
||||||
d="M 193.1,126.74 V 510.7 h 0.006 v 43.543 h 295.82 v -92.338 h -202.74 v -335.16 z"
|
|
||||||
fill="#f83240"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#f83240;fill-opacity:1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 943 B |
Before Width: | Height: | Size: 219 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 4.8 KiB |
77
static/playHLSVideo.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
||||||
|
(function () {
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
||||||
|
videoSources.forEach(function (source) {
|
||||||
|
var playlist = source.src;
|
||||||
|
|
||||||
|
var oldVideo = source.parentNode;
|
||||||
|
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
||||||
|
|
||||||
|
// If HLS is supported natively then don't use hls.js
|
||||||
|
if (oldVideo.canPlayType(source.type)) {
|
||||||
|
if (autoplay) {
|
||||||
|
oldVideo.play();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace video with copy that will have all "source" elements removed
|
||||||
|
var newVideo = oldVideo.cloneNode(true);
|
||||||
|
var allSources = newVideo.querySelectorAll("source");
|
||||||
|
allSources.forEach(function (source) {
|
||||||
|
source.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty source to enable play event
|
||||||
|
newVideo.src = "about:blank";
|
||||||
|
|
||||||
|
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
||||||
|
|
||||||
|
function initializeHls() {
|
||||||
|
newVideo.removeEventListener('play', initializeHls);
|
||||||
|
|
||||||
|
var hls = new Hls({ autoStartLoad: false });
|
||||||
|
hls.loadSource(playlist);
|
||||||
|
hls.attachMedia(newVideo);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||||
|
hls.loadLevel = hls.levels.length - 1;
|
||||||
|
hls.startLoad();
|
||||||
|
newVideo.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
var errorType = data.type;
|
||||||
|
var errorFatal = data.fatal;
|
||||||
|
if (errorFatal) {
|
||||||
|
switch (errorType) {
|
||||||
|
case Hls.ErrorType.NETWORK_ERROR:
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorType.MEDIA_ERROR:
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("HLS error", data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newVideo.addEventListener('play', initializeHls);
|
||||||
|
|
||||||
|
if (autoplay) {
|
||||||
|
newVideo.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var videos = document.querySelectorAll("video.hls_autoplay");
|
||||||
|
videos.forEach(function (video) {
|
||||||
|
video.setAttribute("autoplay", "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// @license-end
|
2894
static/style.css
@ -1,6 +1,6 @@
|
|||||||
/* Black theme setting */
|
/* Black theme setting */
|
||||||
.black {
|
.black {
|
||||||
--accent: #bb2b3b;
|
--accent: #009a9a;
|
||||||
--green: #00a229;
|
--green: #00a229;
|
||||||
--text: white;
|
--text: white;
|
||||||
--foreground: #0f0f0f;
|
--foreground: #0f0f0f;
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
/* Catppuccin theme setting */
|
|
||||||
.catppuccin {
|
|
||||||
--accent: #b4befe; /* lavender */
|
|
||||||
--green: #a6e3a1; /* green */
|
|
||||||
--text: #cdd6f4; /* text */
|
|
||||||
--foreground: #181825; /* mantle */
|
|
||||||
--background: #1e1e2e; /* base */
|
|
||||||
--outside: #11111b; /* crust */
|
|
||||||
--post: #11111b; /* crust */
|
|
||||||
--panel-border: none;
|
|
||||||
--highlighted: #313244; /* surface0 */
|
|
||||||
--visited: #6c7086; /* overlay0 */
|
|
||||||
--shadow: 0 0 0 transparent;
|
|
||||||
|
|
||||||
--nsfw: #fab387; /* peach */
|
|
||||||
--admin: #eba0ac; /* maroon */
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
/* Dark theme setting */
|
/* Dark theme setting */
|
||||||
.dark{
|
.dark{
|
||||||
--accent: #d54455;
|
--accent: aqua;
|
||||||
--green: #5cff85;
|
--green: #5cff85;
|
||||||
--text: white;
|
--text: white;
|
||||||
--foreground: #222;
|
--foreground: #222;
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
/* icebergDark theme setting */
|
|
||||||
.icebergDark {
|
|
||||||
--accent: #85a0c7;
|
|
||||||
--green: #b5bf82;
|
|
||||||
--text: #c6c8d1;
|
|
||||||
--foreground: #454d73;
|
|
||||||
--background: #161821;
|
|
||||||
--outside: #1f2233;
|
|
||||||
--post: #1f2233;
|
|
||||||
--panel-border: 1px solid #454d73;
|
|
||||||
--highlighted: #0f1117;
|
|
||||||
--visited: #0f1117;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
/* Libreddit black theme setting */
|
|
||||||
.libredditBlack {
|
|
||||||
--accent: #009a9a;
|
|
||||||
--green: #00a229;
|
|
||||||
--text: white;
|
|
||||||
--foreground: #0f0f0f;
|
|
||||||
--background: black;
|
|
||||||
--outside: black;
|
|
||||||
--post: black;
|
|
||||||
--panel-border: 2px solid #0f0f0f;
|
|
||||||
--highlighted: #0f0f0f;
|
|
||||||
--visited: #aaa;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
/* Libreddit dark theme setting */
|
|
||||||
.libredditDark{
|
|
||||||
--accent: aqua;
|
|
||||||
--green: #5cff85;
|
|
||||||
--text: white;
|
|
||||||
--foreground: #222;
|
|
||||||
--background: #0f0f0f;
|
|
||||||
--outside: #1f1f1f;
|
|
||||||
--post: #161616;
|
|
||||||
--panel-border: 1px solid #333;
|
|
||||||
--highlighted: #333;
|
|
||||||
--visited: #aaa;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
/* Libreddit light theme setting */
|
|
||||||
.libredditLight {
|
|
||||||
--accent: #009a9a;
|
|
||||||
--green: #00a229;
|
|
||||||
--text: black;
|
|
||||||
--foreground: #f5f5f5;
|
|
||||||
--background: #ddd;
|
|
||||||
--outside: #ececec;
|
|
||||||
--post: #eee;
|
|
||||||
--panel-border: 1px solid #ccc;
|
|
||||||
--highlighted: white;
|
|
||||||
--visited: #555;
|
|
||||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html:has(> .libredditLight) {
|
|
||||||
/* Hint color theme to browser for scrollbar */
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
/* Light theme setting */
|
/* Light theme setting */
|
||||||
.light {
|
.light {
|
||||||
--accent: #bb2b3b;
|
--accent: #009a9a;
|
||||||
--green: #00a229;
|
--green: #00a229;
|
||||||
--text: black;
|
--text: black;
|
||||||
--foreground: #f5f5f5;
|
--foreground: #f5f5f5;
|
||||||
|
@ -1,228 +0,0 @@
|
|||||||
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
|
||||||
let ffmpeg = null;
|
|
||||||
(function () {
|
|
||||||
if (Hls.isSupported()) {
|
|
||||||
|
|
||||||
var downloadsEnabled = document.cookie.split("; ").find((row) => row.startsWith("ffmpeg_video_downloads="))?.split("=")[1] == "on";
|
|
||||||
|
|
||||||
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
|
||||||
videoSources.forEach(function (source) {
|
|
||||||
var playlist = source.src;
|
|
||||||
|
|
||||||
var oldVideo = source.parentNode;
|
|
||||||
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
|
||||||
|
|
||||||
// If HLS is supported natively then don't use hls.js
|
|
||||||
if (oldVideo.canPlayType(source.type) === "probably") {
|
|
||||||
if (autoplay) {
|
|
||||||
oldVideo.play();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace video with copy that will have all "source" elements removed
|
|
||||||
var newVideo = oldVideo.cloneNode(true);
|
|
||||||
var allSources = newVideo.querySelectorAll("source");
|
|
||||||
allSources.forEach(function (source) {
|
|
||||||
source.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Empty source to enable play event
|
|
||||||
newVideo.src = "about:blank";
|
|
||||||
|
|
||||||
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
|
||||||
|
|
||||||
function initializeHls() {
|
|
||||||
newVideo.removeEventListener('play', initializeHls);
|
|
||||||
var hls = new Hls({ autoStartLoad: false });
|
|
||||||
hls.loadSource(playlist);
|
|
||||||
hls.attachMedia(newVideo);
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
|
||||||
hls.loadLevel = hls.levels.length - 1;
|
|
||||||
var availableLevels = hls.levels.map(function(level) {
|
|
||||||
return {
|
|
||||||
height: level.height,
|
|
||||||
width: level.width,
|
|
||||||
bitrate: level.bitrate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
addQualitySelector(newVideo, hls, availableLevels);
|
|
||||||
if (downloadsEnabled){ addVideoDownload(newVideo, hls); }
|
|
||||||
hls.startLoad();
|
|
||||||
newVideo.play();
|
|
||||||
});
|
|
||||||
|
|
||||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
|
||||||
var errorType = data.type;
|
|
||||||
var errorFatal = data.fatal;
|
|
||||||
if (errorFatal) {
|
|
||||||
switch (errorType) {
|
|
||||||
case Hls.ErrorType.NETWORK_ERROR:
|
|
||||||
hls.startLoad();
|
|
||||||
break;
|
|
||||||
case Hls.ErrorType.MEDIA_ERROR:
|
|
||||||
hls.recoverMediaError();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
hls.destroy();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("HLS error", data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadsEnabled){
|
|
||||||
const { fetchFile } = FFmpegUtil;
|
|
||||||
const { FFmpeg } = FFmpegWASM;
|
|
||||||
|
|
||||||
function addVideoDownload(videoElement, hlsInstance) {
|
|
||||||
var mediaStream = [];
|
|
||||||
var downloadButton = document.createElement('button');
|
|
||||||
downloadButton.classList.add('video-options','download');
|
|
||||||
downloadButton.innerText = "⏳"
|
|
||||||
const mergeStreams = async () => {
|
|
||||||
if (ffmpeg === null) {
|
|
||||||
ffmpeg = new FFmpeg();
|
|
||||||
await ffmpeg.load({
|
|
||||||
coreURL: "/ffmpeg/ffmpeg-core.js",
|
|
||||||
});
|
|
||||||
ffmpeg.on("log", ({ message }) => {
|
|
||||||
console.log(message); // This is quite noisy but i will include it
|
|
||||||
})
|
|
||||||
ffmpeg.on("progress", ({ progress, time }) => { // Progress TODO: show progress ring around button not just ⏳
|
|
||||||
// console.log("ffmpeg prog:",progress * 100)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Combine Video Audio Streams
|
|
||||||
await ffmpeg.writeFile("video", await fetchFile(concatBlob(mediaStream['video'])));
|
|
||||||
await ffmpeg.writeFile("audio", await fetchFile(concatBlob(mediaStream['audio'])));
|
|
||||||
console.time('ffmpeg-exec');
|
|
||||||
await ffmpeg.exec(['-i', "video", '-i', "audio",'-c:v', "copy", '-c:a', "aac", 'output.mp4']);
|
|
||||||
console.timeEnd('ffmpeg-exec')
|
|
||||||
|
|
||||||
// Save
|
|
||||||
const toSlug = (str) => {
|
|
||||||
return str
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.replace(/[\W_]+/g, '-')
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/^-+|-+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
|
|
||||||
const data = await ffmpeg.readFile('output.mp4');
|
|
||||||
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
function saveAs(blob, filename) { // Yeah ok...
|
|
||||||
var url = URL.createObjectURL(blob);
|
|
||||||
var a = document.createElement("a");
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.style = "display: none";
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
function concatBlob(inputArray) {
|
|
||||||
var totalLength = inputArray.reduce(function (prev, cur) {
|
|
||||||
return prev + cur.length
|
|
||||||
}, 0);
|
|
||||||
var result = new Uint8Array(totalLength);
|
|
||||||
var offset = 0;
|
|
||||||
inputArray.forEach(function (element) {
|
|
||||||
result.set(element, offset);
|
|
||||||
offset += element.length;
|
|
||||||
});
|
|
||||||
return new Blob([result], {
|
|
||||||
type: 'application/octet-stream'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getStreams() {
|
|
||||||
var video = document.createElement('video');
|
|
||||||
video.autoplay = true;
|
|
||||||
var dataStreams = {
|
|
||||||
'video': [],
|
|
||||||
'audio': []
|
|
||||||
};
|
|
||||||
mediaStream = dataStreams; // Update stream
|
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.BUFFER_APPENDING, function (event, data) {
|
|
||||||
dataStreams[data.type].push(data.data);
|
|
||||||
});
|
|
||||||
var isDownloading = false
|
|
||||||
function startDownload() {
|
|
||||||
if (!isDownloading) { isDownloading = true } else { return }
|
|
||||||
downloadButton.innerText = "⏳"
|
|
||||||
mergeStreams()
|
|
||||||
.then(_ => {
|
|
||||||
isDownloading = false
|
|
||||||
downloadButton.innerText = "⭳"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForLoad() {
|
|
||||||
const poll = resolve => {
|
|
||||||
if(hlsInstance._media.buffered.length === 1 &&
|
|
||||||
hlsInstance._media.buffered.start(0) === 0 &&
|
|
||||||
hlsInstance._media.buffered.end(0) === hlsInstance._media.duration)
|
|
||||||
resolve();
|
|
||||||
else setTimeout(_ => poll(resolve), 400);
|
|
||||||
}
|
|
||||||
return new Promise(poll);
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForLoad(_ => flag === true)
|
|
||||||
.then(_ => {
|
|
||||||
downloadButton.innerText = "⭳"
|
|
||||||
downloadButton.addEventListener('click', startDownload);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.parentNode.appendChild(downloadButton);
|
|
||||||
getStreams()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQualitySelector(videoElement, hlsInstance, availableLevels) {
|
|
||||||
var qualitySelector = document.createElement('select');
|
|
||||||
qualitySelector.classList.add('video-options');
|
|
||||||
var last = availableLevels.length - 1;
|
|
||||||
availableLevels.forEach(function (level, index) {
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = index.toString();
|
|
||||||
var bitrate = (level.bitrate / 1_000).toFixed(0);
|
|
||||||
option.text = level.height + 'p (' + bitrate + ' kbps)';
|
|
||||||
if (index === last) {
|
|
||||||
option.selected = "selected";
|
|
||||||
}
|
|
||||||
qualitySelector.appendChild(option);
|
|
||||||
});
|
|
||||||
qualitySelector.selectedIndex = availableLevels.length - 1;
|
|
||||||
qualitySelector.addEventListener('change', function () {
|
|
||||||
var selectedIndex = qualitySelector.selectedIndex;
|
|
||||||
hlsInstance.nextLevel = selectedIndex;
|
|
||||||
hlsInstance.startLoad();
|
|
||||||
});
|
|
||||||
|
|
||||||
videoElement.parentNode.appendChild(qualitySelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
newVideo.addEventListener('play', initializeHls);
|
|
||||||
|
|
||||||
if (autoplay) {
|
|
||||||
newVideo.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var videos = document.querySelectorAll("video.hls_autoplay");
|
|
||||||
videos.forEach(function (video) {
|
|
||||||
video.setAttribute("autoplay", "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// @license-end
|
|
@ -1,16 +1,13 @@
|
|||||||
{% import "utils.html" as utils %}
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="{% if prefs.fixed_navbar == "on" %}fixed_navbar{% endif %}">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{% block title %}Redlib{% endblock %}</title>
|
<title>{% block title %}Redlib{% endblock %}</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
<meta name="description" content="View on Redlib, an alternative private front-end to Reddit.">
|
<meta name="description" content="View on Redlib, an alternative private front-end to Reddit.">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if crate::utils::disable_indexing() %}
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
{% endif %}
|
|
||||||
<!-- General PWA -->
|
<!-- General PWA -->
|
||||||
<meta name="theme-color" content="#1F1F1F">
|
<meta name="theme-color" content="#1F1F1F">
|
||||||
<!-- iOS Application -->
|
<!-- iOS Application -->
|
||||||
@ -27,20 +24,18 @@
|
|||||||
<link rel="manifest" type="application/json" href="/manifest.json">
|
<link rel="manifest" type="application/json" href="/manifest.json">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
||||||
<link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}">
|
<link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}">
|
||||||
<!-- Video quality -->
|
|
||||||
<div id="video_quality" data-value="{{ prefs.video_quality }}"></div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="
|
<body class="
|
||||||
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
|
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
|
||||||
{% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %} wide{% endif %}
|
{% if prefs.wide == "on" %} wide{% endif %}
|
||||||
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
|
{% if prefs.theme != "system" %} {{ prefs.theme }}{% endif %}
|
||||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||||
<!-- NAVIGATION BAR -->
|
<!-- NAVIGATION BAR -->
|
||||||
<nav class="
|
<nav class="
|
||||||
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
|
||||||
<div id="logo">
|
<div id="logo">
|
||||||
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib.</span></a>
|
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">lib.</span></a>
|
||||||
{% block subscriptions %}{% endblock %}
|
{% block subscriptions %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% block search %}{% endblock %}
|
{% block search %}{% endblock %}
|
||||||
@ -64,13 +59,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{% if prefs.mascot != "none" && prefs.mascot != "" %}
|
|
||||||
<!-- MASCOT -->
|
|
||||||
<div class="mascot">
|
|
||||||
<img src="/mascot/{{ prefs.mascot }}.png">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- MAIN CONTENT -->
|
<!-- MAIN CONTENT -->
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@ -88,7 +76,7 @@
|
|||||||
<a href="/info" title="View instance information">ⓘ View instance info</a>
|
<a href="/info" title="View instance information">ⓘ View instance info</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-button">
|
<div class="footer-button">
|
||||||
<a href="https://git.stardust.wtf/iridium/redsunlib" title="View code on git.stardust.wtf"><> Code</a>
|
<a href="https://github.com/redlib-org/redlib" title="View code on GitHub"><> Code</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
{% if author.flair.flair_parts.len() > 0 %}
|
{% if author.flair.flair_parts.len() > 0 %}
|
||||||
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ post_link }}{{ id }}/?context=3#{{ id }}" class="created" title="{{ created }}">{{ rel_time }}</a>
|
<a href="{{ post_link }}{{ id }}/?context=3" class="created" title="{{ created }}">{{ rel_time }}</a>
|
||||||
{% if edited.0 != "".to_string() %}<span class="edited" title="{{ edited.1 }}">edited {{ edited.0 }}</span>{% endif %}
|
{% if edited.0 != "".to_string() %}<span class="edited" title="{{ edited.1 }}">edited {{ edited.0 }}</span>{% endif %}
|
||||||
{% if !awards.is_empty() && prefs.hide_awards != "on" %}
|
{% if !awards.is_empty() && prefs.hide_awards != "on" %}
|
||||||
<span class="dot">•</span>
|
<span class="dot">•</span>
|
||||||
|
@ -2,23 +2,10 @@
|
|||||||
{% block title %}Error: {{ msg }}{% endblock %}
|
{% block title %}Error: {{ msg }}{% endblock %}
|
||||||
{% block sortstyle %}{% endblock %}
|
{% block sortstyle %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="error">
|
<div id="error">
|
||||||
<h1>{{ msg }}</h1>
|
<h1>{{ msg }}</h1>
|
||||||
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
|
||||||
<br />
|
<br />
|
||||||
<h3 id="update-status"></h3>
|
<h3>Head back <a href="/">home</a>?</h3>
|
||||||
<br />
|
</div>
|
||||||
<h3 id="update-status"></h3>
|
{% endblock %}
|
||||||
<br>
|
|
||||||
<div id="git_commit" data-value="{{ crate::instance_info::INSTANCE_INFO.git_commit }}"></div>
|
|
||||||
<script src="/check_update.js"></script>
|
|
||||||
|
|
||||||
<h3>Expected something to work? Try a random <a id="random-instance">upstream instance.</a></h3>
|
|
||||||
<br />
|
|
||||||
<h3 id="issue_warning" >!! Do <b>NOT</b> open an issue on the <a href="https://github.com/redlib-org/redlib/">redlib repository</a> with a redsunlib specific issue !!</h3>
|
|
||||||
<br />
|
|
||||||
<p id="error-318">If you're getting a "Failed to parse page JSON data" error, please check <a href="https://github.com/redlib-org/redlib/issues/318" target="_blank">#318</a></p>
|
|
||||||
<br />
|
|
||||||
<h3>Head back <a href="/">home</a>?</h3>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -6,11 +6,11 @@
|
|||||||
<h1>
|
<h1>
|
||||||
😱
|
😱
|
||||||
{% if res_type == crate::utils::ResourceType::Subreddit %}
|
{% if res_type == crate::utils::ResourceType::Subreddit %}
|
||||||
r/{{ res }} is a <b class="nsfw-tag">NSFW</b> community!
|
r/{{ res }} is a NSFW community!
|
||||||
{% else if res_type == crate::utils::ResourceType::User %}
|
{% else if res_type == crate::utils::ResourceType::User %}
|
||||||
u/{{ res }}'s content is <b class="nsfw-tag">NSFW</b>!
|
u/{{ res }}'s content is NSFW!
|
||||||
{% else if res_type == crate::utils::ResourceType::Post %}
|
{% else if res_type == crate::utils::ResourceType::Post %}
|
||||||
This post is <b class="nsfw-tag">NSFW</b>!
|
This post is NSFW!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
<br />
|
<br />
|
||||||
@ -20,7 +20,6 @@
|
|||||||
This instance of Redlib is SFW-only.</p>
|
This instance of Redlib is SFW-only.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br>
|
Enable "Show NSFW posts" in <a href="/settings">settings</a> to view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}user's posts or comments{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %}. <br>
|
||||||
<div>Alternatively <a href="/settings/update/?show_nsfw=on&redirect={{self.url[1..self.url.len()]}}">enable NSFW posts</a> now and view this {% if res_type == crate::utils::ResourceType::Subreddit %}subreddit{% else if res_type == crate::utils::ResourceType::User %}profile{% else if res_type == crate::utils::ResourceType::Post %}post{% endif %} immediately</div>
|
|
||||||
{% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %}
|
{% if res_type == crate::utils::ResourceType::Post %} You can also temporarily bypass this gate and view the post by clicking on this <a href="{{url}}&bypass_nsfw_landing">link</a>.{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,34 +10,25 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form id="search_sort">
|
<form id="search_sort">
|
||||||
<div class="search_widget_divider_box">
|
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
||||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q|safe }}" title="Search redlib">
|
{% if sub != "" %}
|
||||||
<div class="search_widget_divider_box">
|
<div id="inside">
|
||||||
{% if sub != "" %}
|
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||||
<div id="inside">
|
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label>
|
||||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
|
||||||
<label for="restrict_sr" class="search_label">in r/{{ sub }}</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %}
|
|
||||||
<select id="sort_options" name="sort" title="Sort results by">
|
|
||||||
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
|
||||||
</select>
|
|
||||||
{% if params.sort != "new" %}
|
|
||||||
<select id="timeframe" name="t" title="Timeframe">
|
|
||||||
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
|
||||||
</select>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button id="sort_submit" class="submit">
|
{% if params.typed == "sr_user" %}<input type="hidden" name="type" value="sr_user">{% endif %}
|
||||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
<select id="sort_options" name="sort" title="Sort results by">
|
||||||
<path d="M20 50 H100" />
|
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||||
<path d="M75 15 L100 50 L75 85" />
|
</select>{% if params.sort != "new" %}<select id="timeframe" name="t" title="Timeframe">
|
||||||
→
|
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||||
</svg>
|
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||||
</button>
|
<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" />
|
||||||
|
→
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if !is_filtered %}
|
{% if !is_filtered %}
|
||||||
@ -106,13 +97,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
{% if prefs.use_hls == "on" %}
|
||||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
|
||||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
|
||||||
<script src="/hls.min.js"></script>
|
<script src="/hls.min.js"></script>
|
||||||
<script src="/videoUtils.js"></script>
|
<script src="/playHLSVideo.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if params.typed != "sr_user" %}
|
{% if params.typed != "sr_user" %}
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
|
|
||||||
{% block title %}Redlib Settings{% endblock %}
|
{% block title %}Redlib Settings{% endblock %}
|
||||||
|
|
||||||
{% block subscriptions %}
|
|
||||||
{% call utils::sub_list("") %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block search %}
|
{% block search %}
|
||||||
{% call utils::search("".to_owned(), "") %}
|
{% call utils::search("".to_owned(), "") %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -23,12 +19,6 @@
|
|||||||
{% call utils::options(prefs.theme, prefs.available_themes, "system") %}
|
{% call utils::options(prefs.theme, prefs.available_themes, "system") %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="mascot">Mascot:</label>
|
|
||||||
<select name="mascot" id="mascot">
|
|
||||||
{% call utils::options(prefs.mascot, prefs.available_mascots, "system") %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Interface</legend>
|
<legend>Interface</legend>
|
||||||
@ -41,39 +31,17 @@
|
|||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="layout">Layout:</label>
|
<label for="layout">Layout:</label>
|
||||||
<select name="layout" id="layout">
|
<select name="layout" id="layout">
|
||||||
{% call utils::options(prefs.layout, ["card", "clean", "compact", "old", "waterfall"], "card") %}
|
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="wide">Wide UI:</label>
|
<label for="wide">Wide UI:</label>
|
||||||
<input type="hidden" value="off" name="wide">
|
<input type="hidden" value="off" name="wide">
|
||||||
<input type="checkbox" name="wide" id="wide" {% if prefs.layout == "old" || prefs.layout == "waterfall" %}disabled{% endif %} {% if prefs.wide == "on" || prefs.layout == "old" || prefs.layout == "waterfall" %}checked{% endif %}>
|
<input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||||
{% if prefs.layout == "old" || prefs.layout == "waterfall" %}<span>ⓘ Wide UI is required for this layout</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="fixed_navbar">Keep navbar fixed</label>
|
|
||||||
<input type="hidden" value="off" name="fixed_navbar">
|
|
||||||
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="hide_sidebar_and_summary">Hide the summary and sidebar</label>
|
|
||||||
<input type="hidden" value="off" name="hide_sidebar_and_summary">
|
|
||||||
<input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
|
|
||||||
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
|
|
||||||
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Content</legend>
|
<legend>Content</legend>
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="video_quality">Video quality:</label>
|
|
||||||
<select name="video_quality" id="video_quality">
|
|
||||||
{% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
|
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
|
||||||
<select name="post_sort">
|
<select name="post_sort">
|
||||||
@ -86,11 +54,6 @@
|
|||||||
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="blur_spoiler">Blur spoiler previews:</label>
|
|
||||||
<input type="hidden" value="off" name="blur_spoiler">
|
|
||||||
<input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
{% if !crate::utils::sfw_only() %}
|
{% if !crate::utils::sfw_only() %}
|
||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="show_nsfw">Show NSFW posts:</label>
|
<label for="show_nsfw">Show NSFW posts:</label>
|
||||||
@ -103,6 +66,30 @@
|
|||||||
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
|
<input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="prefs-group">
|
||||||
|
<label for="autoplay_videos">Autoplay videos</label>
|
||||||
|
<input type="hidden" value="off" name="autoplay_videos">
|
||||||
|
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="prefs-group">
|
||||||
|
<label for="fixed_navbar">Keep navbar fixed</label>
|
||||||
|
<input type="hidden" value="off" name="fixed_navbar">
|
||||||
|
<input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="prefs-group">
|
||||||
|
<label for="use_hls">Use HLS for videos</label>
|
||||||
|
<details id="feeds">
|
||||||
|
<summary>Why?</summary>
|
||||||
|
<div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div>
|
||||||
|
</details>
|
||||||
|
<input type="hidden" value="off" name="use_hls">
|
||||||
|
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="prefs-group">
|
||||||
|
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
|
||||||
|
<input type="hidden" value="off" name="hide_hls_notification">
|
||||||
|
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="hide_awards">Hide awards</label>
|
<label for="hide_awards">Hide awards</label>
|
||||||
<input type="hidden" value="off" name="hide_awards">
|
<input type="hidden" value="off" name="hide_awards">
|
||||||
@ -113,48 +100,15 @@
|
|||||||
<input type="hidden" value="off" name="hide_score">
|
<input type="hidden" value="off" name="hide_score">
|
||||||
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
|
<input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Media</legend>
|
|
||||||
<div class="prefs-group">
|
<div class="prefs-group">
|
||||||
<label for="autoplay_videos">Autoplay videos</label>
|
<label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label>
|
||||||
<input type="hidden" value="off" name="autoplay_videos">
|
<input type="hidden" value="off" name="disable_visit_reddit_confirmation">
|
||||||
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
|
<input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}>
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="use_hls">Use HLS for videos</label>
|
|
||||||
{% if prefs.ffmpeg_video_downloads != "on" %}
|
|
||||||
<details id="feeds">
|
|
||||||
<summary>Why?</summary>
|
|
||||||
<div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div>
|
|
||||||
</details>
|
|
||||||
{% endif %}
|
|
||||||
{% if prefs.ffmpeg_video_downloads == "on" %}<u>ⓘ HLS is required for downloads</u>{% endif %}
|
|
||||||
<input type="hidden" value="off" name="use_hls">
|
|
||||||
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.ffmpeg_video_downloads == "on" %}disabled{% endif %} {% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="ffmpeg_video_downloads">Use FFmpeg to download videos</label>
|
|
||||||
<details id="feeds">
|
|
||||||
<summary>Why?</summary>
|
|
||||||
<div id="feed_list" class="helper">Downloading videos with audio requires ffmpeg (via ffmpeg.wasm) to be enabled to combine video and audio tracks. Therefore, this toggle lets you either use Redlib WebAssembly-free or utilize this feature. (videos will still play with audio)</div>
|
|
||||||
</details>
|
|
||||||
<input type="hidden" value="off" name="ffmpeg_video_downloads">
|
|
||||||
<input type="checkbox" name="ffmpeg_video_downloads" id="ffmpeg_video_downloads" {% if prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="prefs-group">
|
|
||||||
<label for="hide_hls_notification">Hide notification about possible HLS usage</label>
|
|
||||||
<input type="hidden" value="off" name="hide_hls_notification">
|
|
||||||
<input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<input id="save" type="submit" value="Save">
|
<input id="save" type="submit" value="Save">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div id="settings_note">
|
|
||||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
|
||||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
|
||||||
</div>
|
|
||||||
{% if prefs.subscriptions.len() > 0 %}
|
{% if prefs.subscriptions.len() > 0 %}
|
||||||
<div class="prefs" id="settings_subs">
|
<div class="prefs" id="settings_subs">
|
||||||
<legend>Subscribed Feeds</legend>
|
<legend>Subscribed Feeds</legend>
|
||||||
@ -185,6 +139,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="settings_note">
|
||||||
|
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||||
|
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
|
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t" title="Timeframe">
|
||||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
||||||
</select>
|
</select>
|
||||||
<button id="sort_submit" class="submit">
|
<button id="sort_submit" class="submit">
|
||||||
@ -64,29 +64,25 @@
|
|||||||
{% call utils::post_in_list(post) %}
|
{% call utils::post_in_list(post) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
{% if prefs.use_hls == "on" %}
|
||||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
|
||||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
|
||||||
<script src="/hls.min.js"></script>
|
<script src="/hls.min.js"></script>
|
||||||
<script src="/videoUtils.js"></script>
|
<script src="/playHLSVideo.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if !ends.0.is_empty() %}
|
{% if !ends.0.is_empty() %}
|
||||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">< PREV</a>
|
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if !ends.1.is_empty() %}
|
{% if !ends.1.is_empty() %}
|
||||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT ></a>
|
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) && prefs.hide_sidebar_and_summary != "on" %}
|
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %}
|
||||||
<aside>
|
<aside>
|
||||||
{% if is_filtered %}
|
{% if is_filtered %}
|
||||||
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||||
@ -134,16 +130,10 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if crate::utils::enable_rss() %}
|
</div>
|
||||||
<div id="sub_rss">
|
|
||||||
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
|
|
||||||
<button class="subscribe">RSS feed</button >
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details class="panel" id="sidebar" open>
|
<details class="panel" id="sidebar">
|
||||||
<summary id="sidebar_label">Sidebar</summary>
|
<summary id="sidebar_label">Sidebar</summary>
|
||||||
<div id="sidebar_contents">
|
<div id="sidebar_contents">
|
||||||
{{ sub.info|safe }}
|
{{ sub.info|safe }}
|
||||||
|
@ -1,183 +1,135 @@
|
|||||||
{% extends "base.html" %} {% import "utils.html" as utils %} {% block search %}
|
{% extends "base.html" %}
|
||||||
{% call utils::search("".to_owned(), "") %} {% endblock %} {% block title %}{{
|
{% import "utils.html" as utils %}
|
||||||
user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} {%
|
|
||||||
block subscriptions %} {% call utils::sub_list("") %} {% endblock %} {% block
|
|
||||||
body %}
|
|
||||||
<main>
|
|
||||||
{% if !is_filtered %}
|
|
||||||
<div id="column_one">
|
|
||||||
<form id="sort">
|
|
||||||
<div id="listing_options">
|
|
||||||
{% call utils::sort(["/user/", user.name.as_str()].concat(),
|
|
||||||
["overview", "comments", "submitted"], listing) %}
|
|
||||||
</div>
|
|
||||||
<select id="sort_select" name="sort">
|
|
||||||
{% call utils::options(sort.0, ["hot", "new", "top",
|
|
||||||
"controversial"], "") %}</select
|
|
||||||
>{% if sort.0 == "top" || sort.0 == "controversial" %}<select
|
|
||||||
id="timeframe"
|
|
||||||
name="t"
|
|
||||||
>
|
|
||||||
{% call utils::options(sort.1, ["hour", "day", "week", "month",
|
|
||||||
"year", "all"], "all") %}</select
|
|
||||||
>{% endif %}<button id="sort_submit" class="submit">
|
|
||||||
<svg
|
|
||||||
width="15"
|
|
||||||
viewBox="0 0 110 100"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="10"
|
|
||||||
stroke-linecap="round"
|
|
||||||
>
|
|
||||||
<path d="M20 50 H100" />
|
|
||||||
<path d="M75 15 L100 50 L75 85" />
|
|
||||||
→
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if all_posts_hidden_nsfw %}
|
{% block search %}
|
||||||
<center>
|
{% call utils::search("".to_owned(), "") %}
|
||||||
All posts are hidden because they are NSFW. Enable "Show NSFW posts"
|
{% endblock %}
|
||||||
in settings to view.
|
|
||||||
</center>
|
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %}
|
||||||
{% endif %} {% if no_posts %}
|
|
||||||
<center>No posts were found.</center>
|
{% block subscriptions %}
|
||||||
{% endif %} {% if all_posts_filtered %}
|
{% call utils::sub_list("") %}
|
||||||
<center>(All content on this page has been filtered)</center>
|
{% endblock %}
|
||||||
{% else %}
|
|
||||||
<div id="posts">
|
{% block body %}
|
||||||
{% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
|
<main>
|
||||||
"on" %} {% else if !post.title.is_empty() %} {% call
|
{% if !is_filtered %}
|
||||||
utils::post_in_list(post) %} {% else %}
|
<div id="column_one">
|
||||||
<div class="comment user-comment">
|
<form id="sort">
|
||||||
<div class="comment_left">
|
<div id="listing_options">
|
||||||
<p class="comment_score" title="{{ post.score.1 }}">
|
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
|
||||||
{% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
|
</div>
|
||||||
else %} • {% endif %}
|
<select id="sort_select" name="sort">
|
||||||
</p>
|
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %}
|
||||||
<div class="line"></div>
|
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||||
</div>
|
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||||
<details class="comment_right" open>
|
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||||
<summary class="comment_data">
|
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||||
<a
|
<path d="M20 50 H100" />
|
||||||
class="comment_link"
|
<path d="M75 15 L100 50 L75 85" />
|
||||||
href="{{ post.permalink }}#{{ post.id }}"
|
→
|
||||||
title="{{ post.link_title }}"
|
</svg>
|
||||||
>{{ post.link_title }}</a
|
</button>
|
||||||
>
|
</form>
|
||||||
<div class="user_comment_data_divider">
|
|
||||||
<span class="created-in"> in </span>
|
{% if all_posts_hidden_nsfw %}
|
||||||
<a
|
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
|
||||||
class="comment_subreddit"
|
{% endif %}
|
||||||
href="/r/{{ post.community }}"
|
|
||||||
>r/{{ post.community }}</a
|
{% if no_posts %}
|
||||||
>
|
<center>No posts were found.</center>
|
||||||
<span class="dot">•</span>
|
{% endif %}
|
||||||
<span class="created" title="{{ post.created }}"
|
|
||||||
> {{ post.rel_time }}</span
|
{% if all_posts_filtered %}
|
||||||
>
|
<center>(All content on this page has been filtered)</center>
|
||||||
</div>
|
{% else %}
|
||||||
</summary>
|
<div id="posts">
|
||||||
<p class="comment_body">{{ post.body|safe }}</p>
|
{% for post in posts %}
|
||||||
</details>
|
|
||||||
</div>
|
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
||||||
{% endif %} {% endfor %}
|
{% else if !post.title.is_empty() %}
|
||||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
{% call utils::post_in_list(post) %}
|
||||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
{% else %}
|
||||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
<div class="comment">
|
||||||
{% endif %}
|
<div class="comment_left">
|
||||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
<p class="comment_score" title="{{ post.score.1 }}">
|
||||||
<script src="/hls.min.js"></script>
|
{% if prefs.hide_score != "on" %}
|
||||||
<script src="/videoUtils.js"></script>
|
{{ post.score.0 }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
</div>
|
•
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</p>
|
||||||
<footer>
|
<div class="line"></div>
|
||||||
{% if ends.0 != "" %}
|
</div>
|
||||||
<a
|
<details class="comment_right" open>
|
||||||
href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}"
|
<summary class="comment_data">
|
||||||
accesskey="P"
|
<a class="comment_link" href="{{ post.permalink }}">Comment on r/{{ post.community }}</a>
|
||||||
>PREV</a
|
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||||
>
|
</summary>
|
||||||
{% endif %} {% if ends.1 != "" %}
|
<p class="comment_body">{{ post.body|safe }}</p>
|
||||||
<a
|
</details>
|
||||||
href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
|
</div>
|
||||||
accesskey="N"
|
{% endif %}
|
||||||
>NEXT</a
|
{% endfor %}
|
||||||
>
|
{% if prefs.use_hls == "on" %}
|
||||||
{% endif %}
|
<script src="/hls.min.js"></script>
|
||||||
</footer>
|
<script src="/playHLSVideo.js"></script>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
<aside>
|
{% endif %}
|
||||||
{% if is_filtered %}
|
|
||||||
<center>(Content from u/{{ user.name }} has been filtered)</center>
|
<footer>
|
||||||
{% endif %}
|
{% if ends.0 != "" %}
|
||||||
<div class="panel" id="user">
|
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||||
<img
|
{% endif %}
|
||||||
loading="lazy"
|
|
||||||
id="user_icon"
|
{% if ends.1 != "" %}
|
||||||
src="{{ user.icon }}"
|
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||||
alt="User icon"
|
{% endif %}
|
||||||
/>
|
</footer>
|
||||||
<h1 id="user_title">{{ user.title }}</h1>
|
</div>
|
||||||
<p id="user_name">u/{{ user.name }}</p>
|
{% endif %}
|
||||||
<div id="user_description">{{ user.description }}</div>
|
<aside>
|
||||||
<div id="user_details">
|
{% if is_filtered %}
|
||||||
<label>Karma</label>
|
<center>(Content from u/{{ user.name }} has been filtered)</center>
|
||||||
<label>Created</label>
|
{% endif %}
|
||||||
<div>{{ user.karma }}</div>
|
<div class="panel" id="user">
|
||||||
<div>{{ user.created }}</div>
|
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
||||||
</div>
|
<h1 id="user_title">{{ user.title }}</h1>
|
||||||
<div id="user_actions">
|
<p id="user_name">u/{{ user.name }}</p>
|
||||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
<div id="user_description">{{ user.description }}</div>
|
||||||
<div id="user_subscription">
|
<div id="user_details">
|
||||||
{% if prefs.subscriptions.contains(name) %}
|
<label>Karma</label>
|
||||||
<form
|
<label>Created</label>
|
||||||
action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
|
<div>{{ user.karma }}</div>
|
||||||
method="POST"
|
<div>{{ user.created }}</div>
|
||||||
>
|
</div>
|
||||||
<button class="unsubscribe">Unfollow</button>
|
<div id="user_actions">
|
||||||
</form>
|
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||||
{% else %}
|
<div id="user_subscription">
|
||||||
<form
|
{% if prefs.subscriptions.contains(name) %}
|
||||||
action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
|
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
|
||||||
method="POST"
|
<button class="unsubscribe">Unfollow</button>
|
||||||
>
|
</form>
|
||||||
<button class="subscribe">Follow</button>
|
{% else %}
|
||||||
</form>
|
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||||
{% endif %}
|
<button class="subscribe">Follow</button>
|
||||||
</div>
|
</form>
|
||||||
<div id="user_filter">
|
{% endif %}
|
||||||
{% if prefs.filters.contains(name) %}
|
</div>
|
||||||
<form
|
<div id="user_filter">
|
||||||
action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}"
|
{% if prefs.filters.contains(name) %}
|
||||||
method="POST"
|
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST">
|
||||||
>
|
<button class="unfilter">Unfilter</button>
|
||||||
<button class="unfilter">Unfilter</button>
|
</form>
|
||||||
</form>
|
{% else %}
|
||||||
{% else %}
|
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
|
||||||
<form
|
<button class="filter">Filter</button>
|
||||||
action="/r/{{ name }}/filter?redirect={{ redirect_url }}"
|
</form>
|
||||||
method="POST"
|
{% endif %}
|
||||||
>
|
</div>
|
||||||
<button class="filter">Filter</button>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
</aside>
|
||||||
</div>
|
</main>
|
||||||
{% if crate::utils::enable_rss() %}
|
|
||||||
<div id="user_rss">
|
|
||||||
<a
|
|
||||||
href="/u/{{ user.name }}.rss"
|
|
||||||
title="RSS feed for u/{{ user.name }}"
|
|
||||||
>
|
|
||||||
<button class="subscribe">RSS feed</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -62,9 +62,8 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro post(post) -%}
|
{% macro post(post) -%}
|
||||||
{% set post_should_be_blurred = post.flags.spoiler && prefs.blur_spoiler=="on" -%}
|
|
||||||
<!-- POST CONTENT -->
|
<!-- POST CONTENT -->
|
||||||
<div class="post highlighted{% if post_should_be_blurred %} post_blurred{% endif %}">
|
<div class="post highlighted">
|
||||||
<p class="post_header">
|
<p class="post_header">
|
||||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
<span class="dot">•</span>
|
<span class="dot">•</span>
|
||||||
@ -87,14 +86,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<h1 class="post_title">
|
<h1 class="post_title">
|
||||||
|
{{ post.title }}
|
||||||
{% if post.flair.flair_parts.len() > 0 %}
|
{% if post.flair.flair_parts.len() > 0 %}
|
||||||
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||||
class="post_flair"
|
class="post_flair"
|
||||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
|
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ post.title }}
|
|
||||||
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||||
{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- POST MEDIA -->
|
<!-- POST MEDIA -->
|
||||||
@ -103,7 +101,7 @@
|
|||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<a href="{{ post.media.url }}" class="post_media_image" >
|
<a href="{{ post.media.url }}" class="post_media_image" >
|
||||||
{% if post.media.height == 0 || post.media.width == 0 %}
|
{% if post.media.height == 0 || post.media.width == 0 %}
|
||||||
<!-- i.redd.it images special case -->
|
<!-- i.redd.it images speical case -->
|
||||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||||
{% else %}
|
{% else %}
|
||||||
<svg
|
<svg
|
||||||
@ -119,11 +117,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
||||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
|
||||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
|
||||||
<script src="/hls.min.js"></script>
|
<script src="/hls.min.js"></script>
|
||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
|
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
|
||||||
@ -131,7 +125,7 @@
|
|||||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
<script src="/videoUtils.js"></script>
|
<script src="/playHLSVideo.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
||||||
@ -157,10 +151,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- POST BODY -->
|
<!-- POST BODY -->
|
||||||
<div class="post_body">
|
<div class="post_body">{{ post.body|safe }}</div>
|
||||||
{{ post.body|safe }}
|
|
||||||
{% call poll(post) %}
|
|
||||||
</div>
|
|
||||||
<div class="post_score" title="{{ post.score.1 }}">
|
<div class="post_score" title="{{ post.score.1 }}">
|
||||||
{% if prefs.hide_score != "on" %}
|
{% if prefs.hide_score != "on" %}
|
||||||
{{ post.score.0 }}
|
{{ post.score.0 }}
|
||||||
@ -170,32 +161,13 @@
|
|||||||
<span class="label"> Upvotes</span></div>
|
<span class="label"> Upvotes</span></div>
|
||||||
<div class="post_footer">
|
<div class="post_footer">
|
||||||
<ul id="post_links">
|
<ul id="post_links">
|
||||||
<li>
|
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
|
||||||
<a href="{{ post.permalink }}">
|
<li class="mobile_item"><a href="{{ post.permalink }}">link</a></li>
|
||||||
<span class="desktop_item">perma</span>link
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% if post.num_duplicates > 0 %}
|
{% if post.num_duplicates > 0 %}
|
||||||
<li>
|
<li class="desktop_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">duplicates</a></li>
|
||||||
<a href="/r/{{ post.community }}/duplicates/{{ post.id }}">
|
<li class="mobile_item"><a href="/r/{{ post.community }}/duplicates/{{ post.id }}">dupes</a></li>
|
||||||
dup<span class="desktop_item">licat</span>es
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if post.post_type == "link" %}
|
|
||||||
<li class="desktop_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive.is</a></li>
|
|
||||||
<li class="mobile_item"><a target="_blank" href="https://archive.is/latest/{{ post.media.url }}">archive</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% call external_reddit_link(post.permalink) %}
|
{% call external_reddit_link(post.permalink) %}
|
||||||
|
|
||||||
{% if post.media.download_name != "" %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ post.media.url }}" download="{{ post.media.download_name }}">
|
|
||||||
<span class="mobile_item">dl</span>
|
|
||||||
<span class="desktop_item">download</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
|
<p>{{ post.upvote_ratio }}%<span id="upvoted"> Upvoted</span></p>
|
||||||
</div>
|
</div>
|
||||||
@ -203,7 +175,8 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro external_reddit_link(permalink) %}
|
{% macro external_reddit_link(permalink) %}
|
||||||
<li>
|
{% for dev_type in ["desktop", "mobile"] %}
|
||||||
|
<li class="{{ dev_type }}_item">
|
||||||
<a
|
<a
|
||||||
{% if prefs.disable_visit_reddit_confirmation != "on" %}
|
{% if prefs.disable_visit_reddit_confirmation != "on" %}
|
||||||
href="#popup"
|
href="#popup"
|
||||||
@ -217,11 +190,11 @@
|
|||||||
{% call visit_reddit_confirmation(permalink) %}
|
{% call visit_reddit_confirmation(permalink) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro post_in_list(post) -%}
|
{% macro post_in_list(post) -%}
|
||||||
{% set post_should_be_blurred = (post.flags.nsfw && prefs.blur_nsfw=="on") || (post.flags.spoiler && prefs.blur_spoiler=="on") -%}
|
<div class="post {% if post.flags.stickied %}stickied{% endif %}" id="{{ post.id }}">
|
||||||
<div class="post{% if post.flags.stickied %} stickied{% endif %}{% if post_should_be_blurred %} post_blurred{% endif %}" id="{{ post.id }}">
|
|
||||||
<p class="post_header">
|
<p class="post_header">
|
||||||
{% let community -%}
|
{% let community -%}
|
||||||
{% if post.community.starts_with("u_") -%}
|
{% if post.community.starts_with("u_") -%}
|
||||||
@ -249,10 +222,10 @@
|
|||||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
|
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
|
||||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}{% if post.flags.spoiler %} <small class="spoiler">Spoiler</small>{% endif %}
|
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<!-- POST MEDIA/THUMBNAIL -->
|
<!-- POST MEDIA/THUMBNAIL -->
|
||||||
{% if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && post.post_type == "image" %}
|
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
|
||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
|
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height < post.media.width*2 %}short{% endif %}" >
|
||||||
{% if post.media.height == 0 || post.media.width == 0 %}
|
{% if post.media.height == 0 || post.media.width == 0 %}
|
||||||
@ -260,6 +233,7 @@
|
|||||||
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
<img width="100%" height="100%" loading="lazy" alt="Post image" src="{{ post.media.url }}"/>
|
||||||
{% else %}
|
{% else %}
|
||||||
<svg
|
<svg
|
||||||
|
{%if post.flags.nsfw && prefs.blur_nsfw=="on" %}class="post_nsfw_blur"{% endif %}
|
||||||
width="{{ post.media.width }}px"
|
width="{{ post.media.width }}px"
|
||||||
height="{{ post.media.height }}px"
|
height="{{ post.media.height }}px"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
@ -271,22 +245,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card" || prefs.layout == "waterfall") && (post.post_type == "gif" || post.post_type == "video") %}
|
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
|
||||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
|
||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<video class="post_media_video short{% if prefs.autoplay_videos == "on" %} hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||||
|
</div>
|
||||||
|
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
||||||
|
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||||
|
<div class="post_media_content">
|
||||||
|
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="post_media_content">
|
<div class="post_media_content">
|
||||||
<video class="post_media_video short" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||||
</div>
|
</div>
|
||||||
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
|
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else if post.post_type != "self" %}
|
{% else if post.post_type != "self" %}
|
||||||
<a class="post_thumbnail{% if post.thumbnail.url.is_empty() %} no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
<a class="post_thumbnail {% if post.thumbnail.url.is_empty() %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media.url }}{% else %}{{ post.permalink }}{% endif %}" rel="nofollow">
|
||||||
{% if post.thumbnail.url.is_empty() %}
|
{% if post.thumbnail.url.is_empty() %}
|
||||||
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 100 106" width="140" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title>Thumbnail</title>
|
<title>Thumbnail</title>
|
||||||
@ -294,7 +272,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
|
<div style="max-width:{{ post.thumbnail.width }}px;max-height:{{ post.thumbnail.height }}px;">
|
||||||
<svg width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
<svg {% if post.flags.nsfw && prefs.blur_nsfw=="on" %} class="thumb_nsfw_blur" {% endif %} width="{{ post.thumbnail.width }}px" height="{{ post.thumbnail.height }}px" xmlns="http://www.w3.org/2000/svg">
|
||||||
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
|
<image width="100%" height="100%" href="{{ post.thumbnail.url }}"/>
|
||||||
<desc>
|
<desc>
|
||||||
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
|
<img loading="lazy" alt="Thumbnail" src="{{ post.thumbnail.url }}"/>
|
||||||
|