Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
2bacaa163f | |||
48c3a8c0d0 | |||
c23d2dc50b | |||
46dbd88d91 | |||
f0f484288e | |||
90d39b121f | |||
44dee302c9 | |||
c7f9386c01 | |||
66ac72beab | |||
14f9ac4ca7 | |||
6a7f725c12 |
27
.github/workflows/rust.yml
vendored
27
.github/workflows/rust.yml
vendored
@ -21,9 +21,34 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v2.2.1
|
||||
name: Upload a Build Artifact
|
||||
with:
|
||||
name: libreddit
|
||||
path: target/release/libreddit
|
||||
|
||||
- name: Versions
|
||||
id: version
|
||||
run: |
|
||||
echo "::set-output name=version::$(cargo metadata --format-version 1 --no-deps | jq .packages[0].version -r | sed 's/^/v/')"
|
||||
echo "::set-output name=tag::${GITHUB_REF#refs/*/}"
|
||||
|
||||
- name: Calculate SHA512 checksum
|
||||
run: sha512sum target/release/libreddit > libreddit.sha512
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }} - NAME
|
||||
draft: true
|
||||
files: |
|
||||
target/release/libreddit
|
||||
libreddit.sha512
|
||||
body: |
|
||||
- CHANGES
|
||||
|
||||
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 }}
|
||||
|
@ -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.7.0"
|
||||
version = "0.9.0"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -13,10 +13,10 @@ 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"] }
|
||||
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.5", features = ["full"] }
|
||||
hyper-rustls = "0.22.1"
|
||||
route-recognizer = "0.3.0"
|
||||
serde_json = "1.0.64"
|
||||
|
16
Dockerfile
16
Dockerfile
@ -1,17 +1,15 @@
|
||||
FROM rust:latest as builder
|
||||
|
||||
FROM rust:alpine as builder
|
||||
WORKDIR /usr/src/libreddit
|
||||
COPY . .
|
||||
RUN apk add --no-cache g++
|
||||
RUN cargo install --path .
|
||||
|
||||
|
||||
FROM debian:buster-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y libcurl4 && rm -rf /var/lib/apt/lists/*
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache curl
|
||||
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
|
||||
RUN adduser --system --home /nonexistent --no-create-home --disabled-password libreddit
|
||||
USER libreddit
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost:8080/settings || exit 1
|
||||
|
||||
CMD ["libreddit"]
|
||||
CMD ["libreddit"]
|
14
README.md
14
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!
|
||||
|
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
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -32,17 +32,21 @@ 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() {
|
||||
if subscribed.is_empty() {
|
||||
"popular".to_string()
|
||||
let sub = req.param("sub").map_or(
|
||||
if front_page == "default" || front_page.is_empty() {
|
||||
if subscribed.is_empty() {
|
||||
"popular".to_string()
|
||||
} else {
|
||||
subscribed.to_owned()
|
||||
}
|
||||
} else {
|
||||
subscribed.to_owned()
|
||||
}
|
||||
} else {
|
||||
front_page.to_owned()
|
||||
});
|
||||
front_page.to_owned()
|
||||
},
|
||||
String::from,
|
||||
);
|
||||
|
||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
||||
|
||||
@ -88,7 +92,7 @@ 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();
|
||||
let query = req.uri().query().unwrap_or_default().to_string();
|
||||
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
|
||||
|
||||
@ -136,8 +140,8 @@ 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());
|
||||
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 {
|
||||
@ -152,7 +156,7 @@ 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("reddit.com".to_string());
|
||||
let sub = req.param("sub").unwrap_or_else(|| "reddit.com".to_string());
|
||||
|
||||
// Build the Reddit JSON API url
|
||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||
|
@ -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()
|
||||
);
|
||||
|
||||
|
@ -365,6 +365,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 +379,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(),
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,33 @@
|
||||
--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);
|
||||
}
|
||||
/* General */
|
||||
|
||||
::selection {
|
||||
@ -1231,4 +1257,4 @@ td, th {
|
||||
padding: 7px 0px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"], "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">
|
||||
|
Reference in New Issue
Block a user