Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
dc9fbc1a05 | |||
7ae7a88eed | |||
536a766960 | |||
e34329cfee | |||
97a0680bd0 | |||
c1560f4eba | |||
242ffab0da | |||
1211d781d0 | |||
9e4066658c | |||
560de4e91f | |||
bd1c890961 | |||
6f799b2617 | |||
38e176f59f | |||
8248eca95c | |||
ffc3bfe72d | |||
d713746407 | |||
21b45760eb | |||
e3fb93946a | |||
b6134a39d0 | |||
c844655c98 | |||
cac83493da | |||
b47cfd1ba5 | |||
28ca3589ed | |||
3cf787cf98 | |||
46e22cf74e | |||
5c2e134924 | |||
c6244585fa | |||
9f1ba274eb | |||
93ed1c6f0c | |||
6ce82c36fb | |||
2974d92e30 | |||
34dfcb2512 | |||
6b42e97bda | |||
49bfe4d27c | |||
c8965ae51b | |||
0b64a52a63 | |||
a18db1e2b7 | |||
3b53e5be4c | |||
42e8351285 | |||
b3e4b7bfae | |||
4a42a25ed3 | |||
2bacaa163f | |||
48c3a8c0d0 | |||
c23d2dc50b | |||
46dbd88d91 | |||
f0f484288e | |||
90d39b121f | |||
44dee302c9 | |||
c7f9386c01 | |||
66ac72beab | |||
14f9ac4ca7 | |||
6a7f725c12 | |||
2533e8cef5 | |||
772d20615b | |||
0bb1677520 | |||
da4883db29 |
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,24 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve
|
||||
title: Bug Report | [title]
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
## Describe the bug
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
**To reproduce**
|
||||
## To reproduce
|
||||
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
**Expected behavior**
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +1,28 @@
|
||||
---
|
||||
name: Feature request
|
||||
name: 💡 Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: Feature Request | [title]
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
## Is your feature request related to a problem? Please describe.
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
## Describe the solution you'd like
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
-->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
## Describe alternatives you've considered
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
**Additional context**
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: Docker Multi-Architecture Build
|
||||
name: Docker ARM Build
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -30,7 +30,7 @@ jobs:
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./Dockerfile.arm
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: spikecodes/libreddit:latest
|
||||
tags: spikecodes/libreddit:arm
|
26
.github/workflows/rust.yml
vendored
26
.github/workflows/rust.yml
vendored
@ -27,3 +27,29 @@ jobs:
|
||||
with:
|
||||
name: libreddit
|
||||
path: target/release/libreddit
|
||||
|
||||
- name: Versions
|
||||
id: version
|
||||
run: |
|
||||
echo "::set-output name=version::$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')"
|
||||
echo "::set-output name=tag::$(git describe --tags)"
|
||||
|
||||
- name: Calculate SHA512 checksum
|
||||
run: sha512sum target/release/libreddit > libreddit.sha512
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.base_ref != 'master'
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }} - NAME
|
||||
draft: true
|
||||
files: |
|
||||
target/release/libreddit
|
||||
libreddit.sha512
|
||||
body: |
|
||||
- ${{ github.event.head_commit.message }} ${{ github.sha }}
|
||||
|
||||
See full list of changes [here](https://github.com/spikecodes/libreddit/compare/${{ steps.version.outputs.tag }}...${{ steps.version.outputs.version }}).
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1 @@
|
||||
/target
|
||||
Cargo.lock
|
1462
Cargo.lock
generated
Normal file
1462
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@ -3,7 +3,7 @@ name = "libreddit"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/spikecodes/libreddit"
|
||||
version = "0.6.0"
|
||||
version = "0.11.2"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -12,14 +12,14 @@ askama = { version = "0.10.5", default-features = false }
|
||||
async-recursion = "0.3.2"
|
||||
cached = "0.23.0"
|
||||
clap = { version = "2.33.3", default-features = false }
|
||||
regex = "1.4.5"
|
||||
serde = { version = "1.0.124", features = ["derive"] }
|
||||
regex = "1.5.4"
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
cookie = "0.15.0"
|
||||
futures-lite = "1.11.3"
|
||||
hyper = { version = "0.14.4", features = ["full"] }
|
||||
hyper = { version = "0.14.7", features = ["full"] }
|
||||
hyper-rustls = "0.22.1"
|
||||
route-recognizer = "0.3.0"
|
||||
serde_json = "1.0.64"
|
||||
tokio = { version = "1.4.0", features = ["full"] }
|
||||
tokio = { version = "1.5.0", features = ["full"] }
|
||||
time = "0.2.26"
|
||||
url = "2.2.1"
|
||||
url = "2.2.2"
|
||||
|
33
Dockerfile
33
Dockerfile
@ -1,17 +1,36 @@
|
||||
FROM rust:latest as builder
|
||||
####################################################################################################
|
||||
## Builder
|
||||
####################################################################################################
|
||||
FROM rust:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache musl-dev
|
||||
|
||||
WORKDIR /libreddit
|
||||
|
||||
WORKDIR /usr/src/libreddit
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
FROM debian:buster-slim
|
||||
####################################################################################################
|
||||
## Final image
|
||||
####################################################################################################
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apt-get update && apt-get install -y libcurl4 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /usr/local/cargo/bin/libreddit /usr/local/bin/libreddit
|
||||
RUN useradd --system --user-group --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin libreddit
|
||||
# Import ca-certificates from builder
|
||||
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder /libreddit/target/x86_64-unknown-linux-musl/release/libreddit /usr/local/bin/libreddit
|
||||
|
||||
# Use an unprivileged user.
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password libreddit
|
||||
USER libreddit
|
||||
|
||||
# Tell Docker to expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run a healthcheck every minute to make sure Libreddit is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["libreddit"]
|
36
Dockerfile.arm
Normal file
36
Dockerfile.arm
Normal file
@ -0,0 +1,36 @@
|
||||
####################################################################################################
|
||||
## Builder
|
||||
####################################################################################################
|
||||
FROM rust:alpine AS builder
|
||||
|
||||
RUN apk add --no-cache g++
|
||||
|
||||
WORKDIR /usr/src/libreddit
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo install --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/libreddit /usr/local/bin/libreddit
|
||||
|
||||
# Use an unprivileged user.
|
||||
RUN adduser --home /nonexistent --no-create-home --disabled-password libreddit
|
||||
USER libreddit
|
||||
|
||||
# Tell Docker to expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run a healthcheck every minute to make sure Libreddit is functional
|
||||
HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["libreddit"]
|
27
README.md
27
README.md
@ -21,20 +21,6 @@
|
||||
|
||||
---
|
||||
|
||||
## Jump to...
|
||||
- [About](#about)
|
||||
- [Teddit Comparison](#how-does-it-compare-to-teddit)
|
||||
- [Comparison](#comparison)
|
||||
- [Installation](#installation)
|
||||
- [Cargo](#1-cargo)
|
||||
- [Docker](#2-docker)
|
||||
- [AUR](#3-aur)
|
||||
- [GitHub Releases](#4-github-releases)
|
||||
- [Replit](#5-replit)
|
||||
- [Deployment](#deployment)
|
||||
|
||||
---
|
||||
|
||||
# Instances
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here!
|
||||
@ -45,14 +31,19 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
|
||||
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
|
||||
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇺🇸 US | |
|
||||
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ |
|
||||
| [libreddit.himiko.cloud](https://libreddit.himiko.cloud) | 🇫🇮 FI | |
|
||||
| [libreddit.bcow.xyz](https://libreddit.bcow.xyz) | 🇺🇸 US | |
|
||||
| [libreddit.40two.app](https://libreddit.40two.app) | 🇳🇱 NL | |
|
||||
| [reddit.invak.id](https://reddit.invak.id) | 🇧🇬 BG | |
|
||||
| [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
|
||||
| [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
|
||||
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | |
|
||||
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
|
||||
| [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ |
|
||||
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
|
||||
| [libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion](http://libreddit.himiko7xl2skojc6odi7hykl626gt4qki3vxdbv33u2u3af76d6k32ad.onion) | 🇫🇮 FI | |
|
||||
| [dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion](http://dflv6yjt7il3n3tggf4qhcmkzbti2ppytqx3o7pjrzwgntutpewscyid.onion/) | 🇺🇸 US | |
|
||||
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion/) | 🇳🇱 NL | |
|
||||
|
||||
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||
|
||||
@ -80,7 +71,7 @@ Teddit is another awesome open source project designed to provide an alternative
|
||||
|
||||
If you are looking to compare, the biggest differences I have noticed are:
|
||||
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
||||
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Hyper](https://hyper.rs), a speedy and lightweight HTTP server/client implementation.
|
||||
|
||||
---
|
||||
|
||||
@ -169,6 +160,8 @@ docker pull spikecodes/libreddit
|
||||
docker run -d --name libreddit -p 80:8080 spikecodes/libreddit
|
||||
```
|
||||
|
||||
To deploy on `arm64` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:arm`.
|
||||
|
||||
## 3) AUR
|
||||
|
||||
For ArchLinux users, Libreddit is available from the AUR as [`libreddit-git`](https://aur.archlinux.org/packages/libreddit-git).
|
||||
|
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
restart: always
|
||||
container_name: "libreddit"
|
||||
ports:
|
||||
- 8080:8080
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/settings"]
|
||||
interval: 5m
|
||||
timeout: 3s
|
@ -7,7 +7,7 @@ use std::{result::Result, str::FromStr};
|
||||
use crate::server::RequestExt;
|
||||
|
||||
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
|
||||
let mut url = format.to_string();
|
||||
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
|
||||
|
||||
for (name, value) in req.params().iter() {
|
||||
url = url.replace(&format!("{{{}}}", name), value);
|
||||
@ -134,6 +134,6 @@ pub async fn json(path: String) -> Result<Value, String> {
|
||||
Err(e) => err("Failed receiving body from Reddit", e.to_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => err("Couldn't send request to Reddit", e.to_string()),
|
||||
Err(e) => err("Couldn't send request to Reddit", e),
|
||||
}
|
||||
}
|
||||
|
15
src/main.rs
15
src/main.rs
@ -6,7 +6,8 @@
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::similar_names,
|
||||
clippy::cast_possible_wrap
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::find_map
|
||||
)]
|
||||
|
||||
// Reference local files
|
||||
@ -163,7 +164,7 @@ async fn main() {
|
||||
app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed());
|
||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||
app.at("/preview/:loc/:id/:query").get(|r| proxy(r, "https://{loc}view.redd.it/{id}?{query}").boxed());
|
||||
app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed());
|
||||
app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed());
|
||||
app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed());
|
||||
|
||||
@ -204,10 +205,12 @@ async fn main() {
|
||||
.at("/r/:sub/w")
|
||||
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed());
|
||||
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());
|
||||
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());
|
||||
|
||||
app.at("/r/:sub/about/sidebar").get(|r| subreddit::sidebar(r).boxed());
|
||||
|
||||
app.at("/r/:sub/:sort").get(|r| subreddit::community(r).boxed());
|
||||
|
||||
@ -220,10 +223,10 @@ async fn main() {
|
||||
// View Reddit wiki
|
||||
app.at("/w").get(|_| async { Ok(redirect("/wiki".to_string())) }.boxed());
|
||||
app
|
||||
.at("/w/:page")
|
||||
.at("/w/*page")
|
||||
.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/:page").get(|r| subreddit::wiki(r).boxed());
|
||||
app.at("/wiki/*page").get(|r| subreddit::wiki(r).boxed());
|
||||
|
||||
// Search all of Reddit
|
||||
app.at("/search").get(|r| search::find(r).boxed());
|
||||
|
@ -40,7 +40,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
#[cfg(debug_assertions)]
|
||||
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();
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
@ -57,7 +57,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
post,
|
||||
sort,
|
||||
prefs: Preferences::new(req),
|
||||
single_thread: *single_thread,
|
||||
single_thread,
|
||||
})
|
||||
}
|
||||
// If the Reddit API returns an error, exit and send error page to user
|
||||
|
@ -1,5 +1,5 @@
|
||||
// CRATES
|
||||
use crate::utils::{cookie, error, format_num, format_url, param, template, val, Post, Preferences};
|
||||
use crate::utils::{catch_random, cookie, error, format_num, format_url, param, template, val, Post, Preferences};
|
||||
use crate::{client::json, RequestExt};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
@ -38,6 +38,10 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let nsfw_results = if cookie(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
||||
let path = format!("{}.json?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
// Handle random subreddits
|
||||
if let Ok(random) = catch_random(&sub, "/find").await {
|
||||
return Ok(random);
|
||||
}
|
||||
let query = param(&path, "q");
|
||||
|
||||
let sort = if param(&path, "sort").is_empty() {
|
||||
|
@ -61,7 +61,7 @@ impl RequestExt for Request<Body> {
|
||||
}
|
||||
|
||||
fn param(&self, name: &str) -> Option<String> {
|
||||
self.params().find(name).map(|s| s.to_owned())
|
||||
self.params().find(name).map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn set_params(&mut self, params: Params) -> Option<Params> {
|
||||
@ -72,14 +72,14 @@ impl RequestExt for Request<Body> {
|
||||
let mut cookies = Vec::new();
|
||||
if let Some(header) = self.headers().get("Cookie") {
|
||||
for cookie in header.to_str().unwrap_or_default().split("; ") {
|
||||
cookies.push(Cookie::parse(cookie).unwrap_or(Cookie::named("")));
|
||||
cookies.push(Cookie::parse(cookie).unwrap_or_else(|_| Cookie::named("")));
|
||||
}
|
||||
}
|
||||
cookies
|
||||
}
|
||||
|
||||
fn cookie(&self, name: &str) -> Option<Cookie> {
|
||||
self.cookies().iter().find(|c| c.name() == name).map(|c| c.to_owned())
|
||||
self.cookies().iter().find(|c| c.name() == name).map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,9 +171,9 @@ impl Server {
|
||||
parammed.set_params(found.params().to_owned());
|
||||
|
||||
// Run the route's function
|
||||
let yeet = (found.handler().to_owned().to_owned())(parammed);
|
||||
let func = (found.handler().to_owned().to_owned())(parammed);
|
||||
async move {
|
||||
let res: Result<Response<Body>, String> = yeet.await;
|
||||
let res: Result<Response<Body>, String> = func.await;
|
||||
// Add default headers to response
|
||||
res.map(|mut response| {
|
||||
response.headers_mut().extend(headers);
|
||||
|
@ -16,19 +16,6 @@ struct SettingsTemplate {
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Form {
|
||||
theme: Option<String>,
|
||||
front_page: Option<String>,
|
||||
layout: Option<String>,
|
||||
wide: Option<String>,
|
||||
comment_sort: Option<String>,
|
||||
show_nsfw: Option<String>,
|
||||
redirect: Option<String>,
|
||||
subscriptions: Option<String>,
|
||||
}
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
// Retrieve cookies from request "Cookie" header
|
||||
@ -63,7 +50,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
let mut res = redirect("/settings".to_string());
|
||||
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw", "subscriptions"];
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw"];
|
||||
|
||||
for name in names {
|
||||
match form.get(name) {
|
||||
@ -98,7 +85,7 @@ pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
|
||||
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "show_nsfw", "subscriptions"];
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"];
|
||||
|
||||
let path = match form.get("redirect") {
|
||||
Some(value) => format!("/{}/", value),
|
||||
|
118
src/subreddit.rs
118
src/subreddit.rs
@ -1,6 +1,6 @@
|
||||
// CRATES
|
||||
use crate::esc;
|
||||
use crate::utils::{cookie, error, format_num, format_url, param, redirect, rewrite_urls, template, val, Post, Preferences, Subreddit};
|
||||
use crate::utils::{catch_random, cookie, error, format_num, format_url, param, redirect, rewrite_urls, template, val, Post, Preferences, Subreddit};
|
||||
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
@ -32,9 +32,10 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build Reddit API path
|
||||
let subscribed = cookie(&req, "subscriptions");
|
||||
let front_page = cookie(&req, "front_page");
|
||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or("hot".to_string()));
|
||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
||||
|
||||
let sub = req.param("sub").map(String::from).unwrap_or(if front_page == "default" || front_page.is_empty() {
|
||||
let sub = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
|
||||
if subscribed.is_empty() {
|
||||
"popular".to_string()
|
||||
} else {
|
||||
@ -44,6 +45,15 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
front_page.to_owned()
|
||||
});
|
||||
|
||||
// Handle random subreddits
|
||||
if let Ok(random) = catch_random(&sub, "").await {
|
||||
return Ok(random);
|
||||
}
|
||||
|
||||
if req.param("sub").is_some() && sub.starts_with("u_") {
|
||||
return Ok(redirect(["/user/", &sub[2..]].concat()));
|
||||
}
|
||||
|
||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
||||
|
||||
match Post::fetch(&path, String::new()).await {
|
||||
@ -88,14 +98,43 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
|
||||
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
||||
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or_default().to_string();
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
// Handle random subreddits
|
||||
if sub == "random" || sub == "randnsfw" {
|
||||
return Err("Can't subscribe to random subreddit!".to_string());
|
||||
}
|
||||
|
||||
let query = req.uri().query().unwrap_or_default().to_string();
|
||||
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
|
||||
|
||||
let mut sub_list = Preferences::new(req).subscriptions;
|
||||
|
||||
// Retrieve list of posts for these subreddits to extract display names
|
||||
let display = json(format!("/r/{}/hot.json?raw_json=1", sub)).await?;
|
||||
let display_lookup: Vec<(String, &str)> = display["data"]["children"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|post| {
|
||||
let display_name = post["data"]["subreddit"].as_str().unwrap();
|
||||
(display_name.to_lowercase(), display_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Find each subreddit name (separated by '+') in sub parameter
|
||||
for part in sub.split('+') {
|
||||
// Retrieve display name for the subreddit
|
||||
let display;
|
||||
let part = if let Some(&(_, display)) = display_lookup.iter().find(|x| x.0 == part.to_lowercase()) {
|
||||
// This is already known, doesn't require seperate request
|
||||
display
|
||||
} else {
|
||||
// This subreddit display name isn't known, retrieve it
|
||||
let path: String = format!("/r/{}/about.json?raw_json=1", part);
|
||||
display = json(path).await?;
|
||||
display["data"]["display_name"].as_str().ok_or_else(|| "Failed to query subreddit name".to_string())?
|
||||
};
|
||||
|
||||
// Modify sub list based on action
|
||||
if action.contains(&"subscribe".to_string()) && !sub_list.contains(&part.to_owned()) {
|
||||
// Add each sub name to the subscribed list
|
||||
@ -136,14 +175,19 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
||||
}
|
||||
|
||||
pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or("reddit.com".to_string());
|
||||
let page = req.param("page").unwrap_or("index".to_string());
|
||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||
// Handle random subreddits
|
||||
if let Ok(random) = catch_random(&sub, "/wiki").await {
|
||||
return Ok(random);
|
||||
}
|
||||
|
||||
let page = req.param("page").unwrap_or_else(|| "index".to_string());
|
||||
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||
|
||||
match json(path).await {
|
||||
Ok(response) => template(WikiTemplate {
|
||||
sub,
|
||||
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or_default()),
|
||||
wiki: rewrite_urls(response["data"]["content_html"].as_str().unwrap_or("<h3>Wiki not found</h3>")),
|
||||
page,
|
||||
prefs: Preferences::new(req),
|
||||
}),
|
||||
@ -151,6 +195,63 @@ pub async fn wiki(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||
// Handle random subreddits
|
||||
if let Ok(random) = catch_random(&sub, "/about/sidebar").await {
|
||||
return Ok(random);
|
||||
}
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||
|
||||
// Send a request to the url
|
||||
match json(path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => template(WikiTemplate {
|
||||
wiki: format!(
|
||||
"{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
||||
rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
||||
moderators(&sub).await?.join(""),
|
||||
),
|
||||
sub,
|
||||
page: "Sidebar".to_string(),
|
||||
prefs: Preferences::new(req),
|
||||
}),
|
||||
Err(msg) => error(req, msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn moderators(sub: &str) -> Result<Vec<String>, String> {
|
||||
// Retrieve and format the html for the moderators list
|
||||
Ok(
|
||||
moderators_list(sub)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|m| format!("<li><a style=\"color: var(--accent)\" href=\"/u/{name}\">{name}</a></li>", name = m))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn moderators_list(sub: &str) -> Result<Vec<String>, String> {
|
||||
// Build the moderator list URL
|
||||
let path: String = format!("/r/{}/about/moderators.json?raw_json=1", sub);
|
||||
|
||||
// Retrieve response
|
||||
let response = json(path).await?["data"]["children"].clone();
|
||||
Ok(if let Some(response) = response.as_array() {
|
||||
// Traverse json tree and format into list of strings
|
||||
response
|
||||
.iter()
|
||||
.map(|m| m["name"].as_str().unwrap_or(""))
|
||||
.filter(|m| !m.is_empty())
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
}
|
||||
|
||||
// SUBREDDIT
|
||||
async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
||||
// Build the Reddit JSON API url
|
||||
@ -165,7 +266,7 @@ async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
||||
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
||||
|
||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||
let community_icon: &str = res["data"]["community_icon"].as_str().map_or("", |s| s.split('?').collect::<Vec<&str>>()[0]);
|
||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap();
|
||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||
|
||||
let sub = Subreddit {
|
||||
@ -173,6 +274,7 @@ async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
||||
title: esc!(&res, "title"),
|
||||
description: esc!(&res, "public_description"),
|
||||
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
|
||||
moderators: moderators_list(sub).await?,
|
||||
icon: format_url(&icon),
|
||||
members: format_num(members),
|
||||
active: format_num(active),
|
||||
|
@ -23,7 +23,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
// Build the Reddit JSON API path
|
||||
let path = format!(
|
||||
"/user/{}.json?{}&raw_json=1",
|
||||
req.param("name").unwrap_or("reddit".to_string()),
|
||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
||||
req.uri().query().unwrap_or_default()
|
||||
);
|
||||
|
||||
|
38
src/utils.rs
38
src/utils.rs
@ -341,6 +341,7 @@ pub struct Subreddit {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub info: String,
|
||||
pub moderators: Vec<String>,
|
||||
pub icon: String,
|
||||
pub members: (String, String),
|
||||
pub active: (String, String),
|
||||
@ -365,6 +366,7 @@ pub struct Preferences {
|
||||
pub wide: String,
|
||||
pub show_nsfw: String,
|
||||
pub comment_sort: String,
|
||||
pub post_sort: String,
|
||||
pub subscriptions: Vec<String>,
|
||||
}
|
||||
|
||||
@ -378,6 +380,7 @@ impl Preferences {
|
||||
wide: cookie(&req, "wide"),
|
||||
show_nsfw: cookie(&req, "show_nsfw"),
|
||||
comment_sort: cookie(&req, "comment_sort"),
|
||||
post_sort: cookie(&req, "post_sort"),
|
||||
subscriptions: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
||||
}
|
||||
}
|
||||
@ -401,6 +404,19 @@ pub fn cookie(req: &Request<Body>, name: &str) -> String {
|
||||
cookie.value().to_string()
|
||||
}
|
||||
|
||||
// Detect and redirect in the event of a random subreddit
|
||||
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
||||
if (sub == "random" || sub == "randnsfw") && !sub.contains('+') {
|
||||
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub)).await?["data"]["display_name"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
return Ok(redirect(format!("/r/{}{}", new_sub, additional)));
|
||||
} else {
|
||||
return Err("No redirect needed".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Direct urls to proxy if proxy is enabled
|
||||
pub fn format_url(url: &str) -> String {
|
||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||
@ -429,8 +445,8 @@ pub fn format_url(url: &str) -> String {
|
||||
"a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1),
|
||||
"b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1),
|
||||
"emoji.redditmedia.com" => capture(r"https://emoji\.redditmedia\.com/(.*)/(.*)", "/emoji/", 2),
|
||||
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)\?(.*)", "/preview/pre/", 2),
|
||||
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)\?(.*)", "/preview/external-pre/", 2),
|
||||
"preview.redd.it" => capture(r"https://preview\.redd\.it/(.*)", "/preview/pre/", 1),
|
||||
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
|
||||
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
|
||||
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
|
||||
_ => String::new(),
|
||||
@ -442,9 +458,21 @@ pub fn format_url(url: &str) -> String {
|
||||
}
|
||||
|
||||
// Rewrite Reddit links to Libreddit in body of text
|
||||
pub fn rewrite_urls(text: &str) -> String {
|
||||
match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
|
||||
Ok(re) => re.replace_all(text, r#"href="/"#).to_string(),
|
||||
pub fn rewrite_urls(input_text: &str) -> String {
|
||||
let text1 = match Regex::new(r#"href="(https|http|)://(www.|old.|np.|amp.|)(reddit).(com)/"#) {
|
||||
Ok(re) => re.replace_all(input_text, r#"href="/"#).to_string(),
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
|
||||
// Rewrite external media previews to Libreddit
|
||||
match Regex::new(r"https://external-preview\.redd\.it(.*)[^?]") {
|
||||
Ok(re) => {
|
||||
if re.is_match(&text1) {
|
||||
re.replace_all(&text1, format_url(re.find(&text1).unwrap().as_str())).to_string()
|
||||
} else {
|
||||
text1
|
||||
}
|
||||
}
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,75 @@
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dracula theme setting */
|
||||
.dracula {
|
||||
--accent: #bd93f9;
|
||||
--green: #50fa7b;
|
||||
--text: #f8f8f2;
|
||||
--foreground: #3d4051;
|
||||
--background: #282a36;
|
||||
--outside: #44475a;
|
||||
--post: #44475a;
|
||||
--panel-border: 2px solid #44475a;
|
||||
--highlighted: #4e5267;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Nord theme setting */
|
||||
.nord {
|
||||
--accent: #8fbcbb;
|
||||
--green: #a3be8c;
|
||||
--text: #eceff4;
|
||||
--foreground: #3b4252;
|
||||
--background: #2e3440;
|
||||
--outside: #434c5e;
|
||||
--post: #434c5e;
|
||||
--panel-border: 2px solid #4c566a;
|
||||
--highlighted: #3b4252;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Laserwave theme setting */
|
||||
.laserwave {
|
||||
--accent: #eb64b9;
|
||||
--green: #74dfc4;
|
||||
--text: #e0dfe1;
|
||||
--foreground: #302a36;
|
||||
--background: #27212e;
|
||||
--outside: #3e3647;
|
||||
--post: #3e3647;
|
||||
--panel-border: 2px solid #2f2738;
|
||||
--highlighted: #302a36;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Violet theme setting */
|
||||
.violet {
|
||||
--accent: #7c71dd;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #1F2347;
|
||||
--background: #12152b;
|
||||
--outside: #181c3a;
|
||||
--post: #181c3a;
|
||||
--panel-border: 1px solid #1F2347;
|
||||
--highlighted: #1F2347;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Gold theme setting */
|
||||
.gold {
|
||||
--accent: #f2aa4c;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #234;
|
||||
--background: #101820;
|
||||
--outside: #1b2936;
|
||||
--post: #1b2936;
|
||||
--panel-border: 0px solid black;
|
||||
--highlighted: #234;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
@ -281,7 +350,7 @@ aside {
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
#sub_subscription {
|
||||
#sub_subscription, #user_subscription {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@ -1103,6 +1172,8 @@ input[type="submit"] {
|
||||
|
||||
.md table {
|
||||
margin: 5px;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.md code {
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div id="theme">
|
||||
<label for="theme">Theme:</label>
|
||||
<select name="theme">
|
||||
{% call utils::options(prefs.theme, ["system", "light", "dark", "black"], "system") %}
|
||||
{% call utils::options(prefs.theme, ["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"], "system") %}
|
||||
</select>
|
||||
</div>
|
||||
<p>Interface</p>
|
||||
@ -36,6 +36,12 @@
|
||||
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<p>Content</p>
|
||||
<div id="post_sort">
|
||||
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
|
||||
<select name="post_sort">
|
||||
{% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="comment_sort">
|
||||
<label for="comment_sort">Default comment sort:</label>
|
||||
<select name="comment_sort">
|
||||
@ -51,10 +57,10 @@
|
||||
</form>
|
||||
{% if prefs.subscriptions.len() > 0 %}
|
||||
<div class="prefs" id="settings_subs">
|
||||
<p>Subscribed Subreddits</p>
|
||||
<p>Subscribed Feeds</p>
|
||||
{% for sub in prefs.subscriptions %}
|
||||
<div>
|
||||
<span>{{ sub }}</span>
|
||||
<span>{% if sub.starts_with("u_") -%}{{ format!("u/{}", &sub[2..]) }}{% else -%}{{ format!("r/{}", sub) }}{% endif -%}</span>
|
||||
<form action="/r/{{ sub }}/unsubscribe/?redirect=settings" method="POST">
|
||||
<button class="unsubscribe">Unsubscribe</button>
|
||||
</form>
|
||||
|
@ -99,7 +99,17 @@
|
||||
</div>
|
||||
<details class="panel" id="sidebar">
|
||||
<summary id="sidebar_label">Sidebar</summary>
|
||||
<div id="sidebar_contents">{{ sub.info }}</div>
|
||||
<div id="sidebar_contents">
|
||||
{{ sub.info }}
|
||||
<hr>
|
||||
<h2>Moderators</h2>
|
||||
<br>
|
||||
<ul>
|
||||
{% for moderator in sub.moderators %}
|
||||
<li><a style="color: var(--accent)" href="/u/{{ moderator }}">{{ moderator }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
@ -75,6 +75,18 @@
|
||||
<div>{{ user.karma }}</div>
|
||||
<div>{{ user.created }}</div>
|
||||
</div>
|
||||
<div id="user_subscription">
|
||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||
{% if prefs.subscriptions.contains(name) %}
|
||||
<form action="/r/u_{{ user.name }}/unsubscribe" method="POST">
|
||||
<button class="unsubscribe">Unfollow</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/u_{{ user.name }}/subscribe" method="POST">
|
||||
<button class="subscribe">Follow</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
@ -58,7 +58,13 @@
|
||||
{% macro post_in_list(post) -%}
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %}">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
{% let community -%}
|
||||
{% if post.community.starts_with("u_") -%}
|
||||
{% let community = format!("u/{}", &post.community[2..]) -%}
|
||||
{% else -%}
|
||||
{% let community = format!("r/{}", post.community) -%}
|
||||
{% endif -%}
|
||||
<a class="post_subreddit" href="/{{ community }}">{{ community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
<span class="dot">•</span>
|
||||
@ -68,7 +74,8 @@
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||
class="post_flair"
|
||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};"
|
||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
</p>
|
||||
|
Reference in New Issue
Block a user