Compare commits

..

28 Commits

Author SHA1 Message Date
fac56d7f87 Markdown spoilers and post footers on videos 2021-01-08 21:57:36 -08:00
ef1ad17234 Unknown path error handling 2021-01-08 21:11:20 -08:00
b8cdc605a2 Front page config and settings note 2021-01-08 20:55:40 -08:00
ef2f9ad12b Unify preferences under one struct 2021-01-08 17:50:03 -08:00
b13874d0db Add "hide nsfw" option 2021-01-08 17:35:04 -08:00
3d142afd03 Merge pull request from somoso/patch-3
Break word to stop it disappearing on mobile
2021-01-08 17:07:50 -08:00
7fcb7fcfed Break word to stop it disappearing on mobile
This kept happening to me but I couldn't reproduce it in the iPad Simulator. Finally got it nailed down and sorted.

Tested this on Safari (mobile and desktop), Firefox, and Edge browser.
2021-01-08 23:08:58 +00:00
747d5a7c67 Merge pull request from somoso/patch-2
Fix theming for all browsers
2021-01-08 12:56:00 -08:00
770c4d3630 Fix themeing for all browsers
Really noticable on iOS, but ensuring all browsers get the love.

The buttons and input aren't as flat as they usually are on my desktop Firefox. This patch should sort that out.
2021-01-08 20:26:29 +00:00
e7b448a282 Add shadow to navbar 2021-01-07 10:49:10 -08:00
c7c787dff1 Fix comment padding 2021-01-07 10:49:00 -08:00
59a34a0e85 Fixed navbar 2021-01-07 10:46:00 -08:00
6e8cf69227 Fix Default Comment Sorting 2021-01-07 10:32:55 -08:00
3444989f9a Default Comment Sort Setting 2021-01-07 08:38:05 -08:00
7e96bb3d80 Optimize use of Result<> 2021-01-06 21:27:24 -08:00
0adbb1556e Merge branch 'master' of https://github.com/spikecodes/libreddit 2021-01-06 16:45:57 -08:00
710eecdb9d Add issue templates 2021-01-06 16:01:13 -08:00
8a57fa8a1d Remove "Safe" description from README 2021-01-06 14:19:55 -08:00
b33d79ed9b Cache robots.txt 2021-01-06 14:19:10 -08:00
0f506fc41b Cache proxied media 2021-01-06 11:11:04 -08:00
c9cd825d55 Create CSS variables for shadow and text color 2021-01-06 10:51:13 -08:00
e63384e6a6 Update cookie description 2021-01-06 09:54:32 -08:00
3260a4d596 Disable "secure" flag for cookies 2021-01-06 09:52:23 -08:00
da5c4603d9 Switch from chrono to time-rs 2021-01-05 20:01:21 -08:00
b50fa6f3ae Settings Button 2021-01-05 18:16:32 -08:00
aa7b4b2af7 Settings with Layouts 2021-01-05 18:04:49 -08:00
2b0193f5ea Fix proxying of NSFW images 2021-01-05 08:15:34 -08:00
2185d895c0 Prevent user datetimes from floating 2021-01-04 21:32:22 -08:00
20 changed files with 675 additions and 395 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**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**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

348
Cargo.lock generated

@ -75,7 +75,7 @@ dependencies = [
"log",
"mime",
"percent-encoding",
"pin-project 1.0.2",
"pin-project 1.0.3",
"rand",
"regex",
"serde",
@ -83,7 +83,7 @@ dependencies = [
"serde_urlencoded",
"sha-1",
"slab",
"time 0.2.23",
"time",
]
[[package]]
@ -92,8 +92,8 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
dependencies = [
"quote 1.0.8",
"syn 1.0.57",
"quote",
"syn",
]
[[package]]
@ -247,14 +247,14 @@ dependencies = [
"fxhash",
"log",
"mime",
"pin-project 1.0.2",
"pin-project 1.0.3",
"regex",
"rustls",
"serde",
"serde_json",
"serde_urlencoded",
"socket2",
"time 0.2.23",
"time",
"tinyvec",
"url",
]
@ -265,9 +265,9 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -295,10 +295,16 @@ dependencies = [
]
[[package]]
name = "askama"
version = "0.8.0"
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dc2a4b6d7f812d2b13d251ae792caecebd635d6401761162d4b71d5ebe1a010"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "askama"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134"
dependencies = [
"askama_derive",
"askama_escape",
@ -307,34 +313,36 @@ dependencies = [
[[package]]
name = "askama_derive"
version = "0.8.0"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ee2fff0f22ad5d215cace1227cd036c28e81e26206763bb837b6d0e766c87d"
checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522"
dependencies = [
"askama_shared",
"nom",
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
"proc-macro2",
"syn",
]
[[package]]
name = "askama_escape"
version = "0.2.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0de942230b5beedaa9e1d64df5b76fa1c97002e4c7982897be899cccf40621d"
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
[[package]]
name = "askama_shared"
version = "0.8.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6dfa6b6d254fd066a8bbed9a8f913123e3f701db89216ad4f0aff04ad87718c"
checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc"
dependencies = [
"askama_escape",
"humansize",
"nom",
"num-traits",
"percent-encoding",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn",
"toml",
]
@ -344,9 +352,9 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -355,9 +363,9 @@ version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -429,6 +437,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bitvec"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
@ -503,24 +523,11 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time 0.1.44",
"winapi 0.3.9",
]
[[package]]
name = "const_fn"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
[[package]]
name = "cookie"
@ -529,8 +536,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f"
dependencies = [
"percent-encoding",
"time 0.2.23",
"version_check 0.9.2",
"time",
"version_check",
]
[[package]]
@ -560,9 +567,9 @@ version = "0.99.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -602,9 +609,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
dependencies = [
"heck",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -651,6 +658,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.8"
@ -694,9 +707,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
dependencies = [
"proc-macro-hack",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -727,7 +740,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
"pin-project 1.0.2",
"pin-project 1.0.3",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
@ -750,7 +763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
"typenum",
"version_check 0.9.2",
"version_check",
]
[[package]]
@ -761,7 +774,7 @@ checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
"wasi",
]
[[package]]
@ -880,7 +893,7 @@ dependencies = [
"httparse",
"httpdate",
"itoa",
"pin-project 1.0.2",
"pin-project 1.0.3",
"socket2",
"tokio",
"tower-service",
@ -999,10 +1012,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.81"
name = "lexical-core"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if 0.1.10",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
[[package]]
name = "libreddit"
@ -1012,11 +1038,11 @@ dependencies = [
"askama",
"async-recursion",
"base64 0.13.0",
"chrono",
"regex",
"reqwest",
"serde",
"serde_json",
"time",
"url",
]
@ -1152,22 +1178,14 @@ dependencies = [
[[package]]
name = "nom"
version = "4.2.3"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0"
dependencies = [
"bitvec",
"lexical-core",
"memchr",
"version_check 0.1.5",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
"version_check",
]
[[package]]
@ -1249,11 +1267,11 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7"
checksum = "5a83804639aad6ba65345661744708855f9fbcb71176ea8d28d05aeb11d975e7"
dependencies = [
"pin-project-internal 1.0.2",
"pin-project-internal 1.0.3",
]
[[package]]
@ -1262,20 +1280,20 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-internal"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f"
checksum = "b7bcc46b8f73443d15bc1c5fecbb315718491fa9187fa483f0e359323cde8b3a"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -1286,9 +1304,9 @@ checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b"
[[package]]
name = "pin-project-lite"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c"
checksum = "e36743d754ccdf9954c2e352ce2d4b106e024c814f6499c2dadff80da9a442d8"
[[package]]
name = "pin-utils"
@ -1314,22 +1332,13 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
dependencies = [
"unicode-xid 0.1.0",
]
[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid 0.2.1",
"unicode-xid",
]
[[package]]
@ -1338,24 +1347,21 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
dependencies = [
"proc-macro2 0.4.30",
]
[[package]]
name = "quote"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
dependencies = [
"proc-macro2 1.0.24",
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.7.3"
@ -1443,7 +1449,7 @@ dependencies = [
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite 0.2.0",
"pin-project-lite 0.2.1",
"rustls",
"serde",
"serde_urlencoded",
@ -1562,9 +1568,9 @@ version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -1653,9 +1659,15 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0"
dependencies = [
"version_check 0.9.2",
"version_check",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stdweb"
version = "0.4.20"
@ -1676,11 +1688,11 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn 1.0.57",
"syn",
]
[[package]]
@ -1690,13 +1702,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
dependencies = [
"base-x",
"proc-macro2 1.0.24",
"quote 1.0.8",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"serde_json",
"sha1",
"syn 1.0.57",
"syn",
]
[[package]]
@ -1707,25 +1719,20 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "syn"
version = "0.15.44"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"unicode-xid 0.1.0",
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "syn"
version = "1.0.57"
name = "tap"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"unicode-xid 0.2.1",
]
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
[[package]]
name = "thiserror"
@ -1742,16 +1749,16 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
dependencies = [
"lazy_static",
]
@ -1765,17 +1772,6 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.2.23"
@ -1787,7 +1783,7 @@ dependencies = [
"standback",
"stdweb",
"time-macros",
"version_check 0.9.2",
"version_check",
"winapi 0.3.9",
]
@ -1808,10 +1804,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
dependencies = [
"proc-macro-hack",
"proc-macro2 1.0.24",
"quote 1.0.8",
"proc-macro2",
"quote",
"standback",
"syn 1.0.57",
"syn",
]
[[package]]
@ -1878,9 +1874,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.4.10"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
@ -1899,7 +1895,7 @@ checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3"
dependencies = [
"cfg-if 1.0.0",
"log",
"pin-project-lite 0.2.0",
"pin-project-lite 0.2.1",
"tracing-core",
]
@ -1980,7 +1976,7 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check 0.9.2",
"version_check",
]
[[package]]
@ -2007,12 +2003,6 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unicode-xid"
version = "0.2.1"
@ -2037,12 +2027,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
version = "0.9.2"
@ -2065,12 +2049,6 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.69"
@ -2092,9 +2070,9 @@ dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
@ -2116,7 +2094,7 @@ version = "0.2.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
dependencies = [
"quote 1.0.8",
"quote",
"wasm-bindgen-macro-support",
]
@ -2126,9 +2104,9 @@ version = "0.2.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn 1.0.57",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2235,3 +2213,9 @@ dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"

@ -9,12 +9,12 @@ edition = "2018"
[dependencies]
base64 = "0.13.0"
actix-web = { version = "3.2.0", features = ["rustls"] }
actix-web = { version = "3.3.2", features = ["rustls"] }
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
askama = "0.8.0"
serde = "1.0.117"
askama = "0.10.5"
serde = { version = "1.0.118", default_features = false, features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.19"
async-recursion = "0.3.1"
url = "2.2.0"
regex = "1"
regex = "1.4.2"
time = "0.2.23"

@ -7,7 +7,6 @@ Libre + Reddit = [Libreddit](https://libredd.it)
- 🚀 Fast: written in Rust for blazing fast speeds and safety
- ☁️ Light: no JavaScript, no ads, no tracking
- 🕵 Private: all requests are proxied through the server, including media
- 🦺 Safe: does not rely on Reddit OAuth or require a Reddit API Key
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libredd.it/r/unpopularopinion) without being [tracked](#reddit).
@ -128,7 +127,7 @@ For transparency, I hope to describe all the ways Libreddit handles user privacy
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
**Cookies:** Libreddit uses no cookies currently but eventually, I plan to add a configuration page where users can store an optional cookie to save their preferred theme, default sorting algorithm, or default layout.
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libredd.it/settings). This is not a cross-site cookie and the cookie holds no personal data, only a value of the possible layout.
**Hosting:** The official instances (`libredd.it` and `libreddit.spike.codes`) are hosted on [Repl.it](https://repl.it/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting and browsing through Tor are welcomed.

@ -5,7 +5,7 @@ use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServ
mod post;
mod proxy;
mod search;
// mod settings;
mod settings;
mod subreddit;
mod user;
mod utils;
@ -16,7 +16,9 @@ async fn style() -> HttpResponse {
}
async fn robots() -> HttpResponse {
HttpResponse::Ok().body(include_str!("../static/robots.txt"))
HttpResponse::Ok()
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(include_str!("../static/robots.txt"))
}
#[get("/favicon.ico")]
@ -32,8 +34,7 @@ async fn main() -> std::io::Result<()> {
if args.len() > 1 {
for arg in args {
if arg.starts_with("--address=") || arg.starts_with("-a=") {
let split: Vec<&str> = arg.split('=').collect();
address = split[1].to_string();
address = arg.split('=').collect::<Vec<&str>>()[1].to_string();
}
}
}
@ -46,14 +47,14 @@ async fn main() -> std::io::Result<()> {
// TRAILING SLASH MIDDLEWARE
.wrap(NormalizePath::default())
// DEFAULT SERVICE
.default_service(web::get().to(utils::error))
.default_service(web::get().to(|| utils::error("Nothing here".to_string())))
// GENERAL SERVICES
.route("/style.css/", web::get().to(style))
.route("/favicon.ico/", web::get().to(HttpResponse::Ok))
.route("/robots.txt/", web::get().to(robots))
// SETTINGS SERVICE
// .route("/settings/", web::get().to(settings::get))
// .route("/settings/save/", web::post().to(settings::set))
.route("/settings/", web::get().to(settings::get))
.route("/settings/", web::post().to(settings::set))
// PROXY SERVICE
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
// SEARCH SERVICES

@ -1,11 +1,11 @@
// CRATES
use crate::utils::{error, format_num, format_url, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
use actix_web::{HttpRequest, HttpResponse, Result};
use crate::utils::{cookie, error, format_num, format_url, media, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
use actix_web::{HttpRequest, HttpResponse};
use async_recursion::async_recursion;
use askama::Template;
use chrono::{TimeZone, Utc};
use time::OffsetDateTime;
// STRUCTS
#[derive(Template)]
@ -17,8 +17,20 @@ struct PostTemplate {
}
pub async fn item(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
let sort = param(&path, "sort");
// Build Reddit API path
let mut path: String = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
// Set sort to sort query parameter
let mut sort: String = param(&path, "sort");
// Grab default comment sort method from Cookies
let default_sort = cookie(&req, "comment_sort");
// If there's no sort query but there's a default sort, set sort to default_sort
if sort.is_empty() && !default_sort.is_empty() {
sort = default_sort;
path = format!("{}.json?{}&sort={}&raw_json=1", req.path(), req.query_string(), sort);
}
// Log the post ID being fetched in debug mode
#[cfg(debug_assertions)]
@ -29,8 +41,8 @@ pub async fn item(req: HttpRequest) -> HttpResponse {
// Otherwise, grab the JSON output from the request
Ok(res) => {
// Parse the JSON into Post and Comment structs
let post = parse_post(&res[0]).await.unwrap();
let comments = parse_comments(&res[1]).await.unwrap();
let post = parse_post(&res[0]).await;
let comments = parse_comments(&res[1]).await;
// Use the Post and Comment structs to generate a website to show users
let s = PostTemplate { comments, post, sort }.render().unwrap();
@ -41,28 +53,8 @@ pub async fn item(req: HttpRequest) -> HttpResponse {
}
}
// UTILITIES
async fn media(data: &serde_json::Value) -> (String, String) {
let post_type: &str;
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
post_type = "video";
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
post_type = "video";
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
post_type = "image";
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap_or_default().to_string())
} else {
post_type = "link";
data["url"].as_str().unwrap_or_default().to_string()
};
(post_type.to_string(), url)
}
// POSTS
async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
async fn parse_post(json: &serde_json::Value) -> Post {
// Retrieve post (as opposed to comments) from JSON
let post: &serde_json::Value = &json["data"]["children"][0];
@ -73,10 +65,10 @@ async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
// Determine the type of media along with the media URL
let media = media(&post["data"]).await;
let (post_type, media) = media(&post["data"]).await;
// Build a post using data parsed from Reddit post API
Ok(Post {
Post {
id: val(post, "id"),
title: val(post, "title"),
community: val(post, "subreddit"),
@ -90,7 +82,8 @@ async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
permalink: val(post, "permalink"),
score: format_num(score),
upvote_ratio: ratio as i64,
post_type: media.0,
post_type,
thumbnail: format_url(val(post, "thumbnail")),
flair: Flair(
val(post, "link_flair_text"),
val(post, "link_flair_background_color"),
@ -104,14 +97,14 @@ async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
},
media: media.1,
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
})
media,
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d %Y %H:%M UTC"),
}
}
// COMMENTS
#[async_recursion]
async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'static str> {
async fn parse_comments(json: &serde_json::Value) -> Vec<Comment> {
// Separate the comment JSON into a Vector of comments
let comment_data = json["data"]["children"].as_array().unwrap();
@ -128,7 +121,7 @@ async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'stat
let body = rewrite_url(&val(comment, "body_html"));
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
parse_comments(&comment["data"]["replies"]).await.unwrap_or_default()
parse_comments(&comment["data"]["replies"]).await
} else {
Vec::new()
};
@ -138,7 +131,7 @@ async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'stat
body,
author: val(comment, "author"),
score: format_num(score),
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d %Y %H:%M UTC"),
replies,
flair: Flair(
val(comment, "author_flair_text"),
@ -148,5 +141,5 @@ async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'stat
});
}
Ok(comments)
comments
}

@ -28,19 +28,20 @@ pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse>
let domain = url.domain().unwrap_or_default();
if domains.contains(&domain) {
Client::default()
.get(media.replace("&amp;", "&"))
.send()
.await
.map_err(Error::from)
.map(|res| HttpResponse::build(res.status()).streaming(res))
Client::default().get(media.replace("&amp;", "&")).send().await.map_err(Error::from).map(|res| {
HttpResponse::build(res.status())
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned())
.header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned())
.streaming(res)
})
} else {
Err(error::ErrorForbidden("Resource must be from Reddit"))
}
}
Err(_) => Err(error::ErrorBadRequest("Can't parse encoded base64 URL")),
Err(_) => Err(error::ErrorBadRequest("Can't parse base64 into URL")),
}
}
Err(_) => Err(error::ErrorBadRequest("Can't decode base64 URL")),
Err(_) => Err(error::ErrorBadRequest("Can't decode base64")),
}
}

@ -1,5 +1,5 @@
// CRATES
use crate::utils::{error, fetch_posts, param, Post};
use crate::utils::{error, fetch_posts, param, prefs, Post, Preferences};
use actix_web::{HttpRequest, HttpResponse};
use askama::Template;
@ -19,6 +19,7 @@ struct SearchTemplate {
posts: Vec<Post>,
sub: String,
params: SearchParams,
prefs: Preferences,
}
// SERVICES
@ -32,18 +33,19 @@ pub async fn find(req: HttpRequest) -> HttpResponse {
let sub = req.match_info().get("sub").unwrap_or("").to_string();
match fetch_posts(&path, String::new()).await {
Ok(posts) => HttpResponse::Ok().content_type("text/html").body(
Ok((posts, after)) => HttpResponse::Ok().content_type("text/html").body(
SearchTemplate {
posts: posts.0,
posts,
sub,
params: SearchParams {
q: param(&path, "q"),
sort,
t: param(&path, "t"),
before: param(&path, "after"),
after: posts.1,
after,
restrict_sr: param(&path, "restrict_sr"),
},
prefs: prefs(req),
}
.render()
.unwrap(),

@ -1,48 +1,57 @@
// // CRATES
// use crate::utils::cookies;
// use actix_web::{cookie::Cookie, web::Form, HttpRequest, HttpResponse, Result}; // http::Method,
// use askama::Template;
// CRATES
use crate::utils::{prefs, Preferences};
use actix_web::{cookie::Cookie, web::Form, HttpMessage, HttpRequest, HttpResponse};
use askama::Template;
use time::{Duration, OffsetDateTime};
// // STRUCTS
// #[derive(Template)]
// #[template(path = "settings.html", escape = "none")]
// struct SettingsTemplate {
// pref_nsfw: String,
// }
// STRUCTS
#[derive(Template)]
#[template(path = "settings.html")]
struct SettingsTemplate {
prefs: Preferences,
}
// #[derive(serde::Deserialize)]
// pub struct Preferences {
// pref_nsfw: Option<String>,
// }
#[derive(serde::Deserialize)]
pub struct SettingsForm {
front_page: Option<String>,
layout: Option<String>,
comment_sort: Option<String>,
hide_nsfw: Option<String>,
}
// // FUNCTIONS
// FUNCTIONS
// // Retrieve cookies from request "Cookie" header
// pub async fn get(req: HttpRequest) -> Result<HttpResponse> {
// let cookies = cookies(req);
// Retrieve cookies from request "Cookie" header
pub async fn get(req: HttpRequest) -> HttpResponse {
let s = SettingsTemplate { prefs: prefs(req) }.render().unwrap();
HttpResponse::Ok().content_type("text/html").body(s)
}
// let pref_nsfw: String = cookies.get("pref_nsfw").unwrap_or(&String::new()).to_owned();
// Set cookies using response "Set-Cookie" header
pub async fn set(req: HttpRequest, form: Form<SettingsForm>) -> HttpResponse {
let mut res = HttpResponse::Found();
// let s = SettingsTemplate { pref_nsfw }.render().unwrap();
// Ok(HttpResponse::Ok().content_type("text/html").body(s))
// }
let names = vec!["front_page", "layout", "comment_sort", "hide_nsfw"];
let values = vec![&form.front_page, &form.layout, &form.comment_sort, &form.hide_nsfw];
// // Set cookies using response "Set-Cookie" header
// pub async fn set(form: Form<Preferences>) -> HttpResponse {
// let nsfw: Cookie = match &form.pref_nsfw {
// Some(value) => Cookie::build("pref_nsfw", value).path("/").secure(true).http_only(true).finish(),
// None => Cookie::build("pref_nsfw", "").finish(),
// };
for (i, name) in names.iter().enumerate() {
match values[i] {
Some(value) => res.cookie(
Cookie::build(name.to_owned(), value)
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.finish(),
),
None => match HttpMessage::cookie(&req, name.to_owned()) {
Some(cookie) => res.del_cookie(&cookie),
None => &mut res,
},
};
}
// let body = SettingsTemplate {
// pref_nsfw: form.pref_nsfw.clone().unwrap_or_default(),
// }
// .render()
// .unwrap();
// HttpResponse::Found()
// .content_type("text/html")
// .set_header("Set-Cookie", nsfw.to_string())
// .set_header("Location", "/settings")
// .body(body)
// }
res
.content_type("text/html")
.set_header("Location", "/settings")
.body(r#"Redirecting to <a href="/settings">settings</a>..."#)
}

@ -1,5 +1,5 @@
// CRATES
use crate::utils::{error, fetch_posts, format_num, format_url, param, request, rewrite_url, val, Post, Subreddit};
use crate::utils::{cookie, error, fetch_posts, format_num, format_url, param, prefs, request, rewrite_url, val, Post, Preferences, Subreddit};
use actix_web::{HttpRequest, HttpResponse, Result};
use askama::Template;
@ -11,6 +11,7 @@ struct SubredditTemplate {
posts: Vec<Post>,
sort: (String, String),
ends: (String, String),
prefs: Preferences,
}
#[derive(Template)]
@ -24,22 +25,28 @@ struct WikiTemplate {
// SERVICES
pub async fn page(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}", req.path(), req.query_string());
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
let default = cookie(&req, "front_page");
let sub_name = req
.match_info()
.get("sub")
.unwrap_or(if default.is_empty() { "popular" } else { default.as_str() })
.to_string();
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
let sub_result = if !&sub.contains('+') && sub != "popular" {
subreddit(&sub).await.unwrap_or_default()
let sub = if !&sub_name.contains('+') && sub_name != "popular" && sub_name != "all" {
subreddit(&sub_name).await.unwrap_or_default()
} else {
Subreddit::default()
};
match fetch_posts(&path, String::new()).await {
Ok(items) => {
Ok((posts, after)) => {
let s = SubredditTemplate {
sub: sub_result,
posts: items.0,
sub,
posts,
sort: (sort, param(&path, "t")),
ends: (param(&path, "after"), items.1),
ends: (param(&path, "after"), after),
prefs: prefs(req),
}
.render()
.unwrap();

@ -1,8 +1,8 @@
// CRATES
use crate::utils::{error, fetch_posts, format_url, nested_val, param, request, Post, User};
use crate::utils::{error, fetch_posts, format_url, nested_val, param, prefs, request, Post, Preferences, User};
use actix_web::{HttpRequest, HttpResponse, Result};
use askama::Template;
use chrono::{TimeZone, Utc};
use time::OffsetDateTime;
// STRUCTS
#[derive(Template)]
@ -12,6 +12,7 @@ struct UserTemplate {
posts: Vec<Post>,
sort: (String, String),
ends: (String, String),
prefs: Preferences,
}
// FUNCTIONS
@ -24,16 +25,17 @@ pub async fn profile(req: HttpRequest) -> HttpResponse {
let username = req.match_info().get("username").unwrap_or("").to_string();
// Request user profile data and user posts/comments from Reddit
let user = user(&username).await;
let user = user(&username).await.unwrap_or_default();
let posts = fetch_posts(&path, "Comment".to_string()).await;
match posts {
Ok(items) => {
Ok((posts, after)) => {
let s = UserTemplate {
user: user.unwrap(),
posts: items.0,
user,
posts,
sort: (sort, param(&path, "t")),
ends: (param(&path, "after"), items.1),
ends: (param(&path, "after"), after),
prefs: prefs(req),
}
.render()
.unwrap();
@ -49,29 +51,25 @@ async fn user(name: &str) -> Result<User, &'static str> {
// Build the Reddit JSON API path
let path: String = format!("user/{}/about.json", name);
let res;
// Send a request to the url
match request(&path).await {
// If success, receive JSON in response
Ok(response) => {
res = response;
Ok(res) => {
// Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// Parse the JSON output into a User struct
Ok(User {
name: name.to_string(),
title: nested_val(&res, "subreddit", "title"),
icon: format_url(nested_val(&res, "subreddit", "icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
banner: nested_val(&res, "subreddit", "banner_img"),
description: nested_val(&res, "subreddit", "public_description"),
})
}
// If the Reddit API returns an error, exit this function
Err(msg) => return Err(msg),
}
// Grab creation date as unix timestamp
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
// Parse the JSON output into a User struct
Ok(User {
name: name.to_string(),
title: nested_val(&res, "subreddit", "title"),
icon: format_url(nested_val(&res, "subreddit", "icon_img")),
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(),
banner: nested_val(&res, "subreddit", "banner_img"),
description: nested_val(&res, "subreddit", "public_description"),
})
}

@ -1,16 +1,14 @@
// use std::collections::HashMap;
//
// CRATES
//
use actix_web::{HttpResponse, Result};
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result};
use askama::Template;
use base64::encode;
use chrono::{TimeZone, Utc};
use regex::Regex;
use serde_json::from_str;
use std::collections::HashMap;
use time::OffsetDateTime;
use url::Url;
// use surf::{client, get, middleware::Redirect};
//
// STRUCTS
@ -37,6 +35,7 @@ pub struct Post {
pub post_type: String,
pub flair: Flair,
pub flags: Flags,
pub thumbnail: String,
pub media: String,
pub time: String,
}
@ -52,6 +51,7 @@ pub struct Comment {
pub replies: Vec<Comment>,
}
#[derive(Default)]
// User struct containing metadata about user
pub struct User {
pub name: String,
@ -93,38 +93,42 @@ pub struct ErrorTemplate {
pub message: String,
}
pub struct Preferences {
pub front_page: String,
pub layout: String,
pub hide_nsfw: String,
pub comment_sort: String,
}
//
// FORMATTING
//
// Build preferences from cookies
pub fn prefs(req: HttpRequest) -> Preferences {
Preferences {
front_page: cookie(&req, "front_page"),
layout: cookie(&req, "layout"),
hide_nsfw: cookie(&req, "hide_nsfw"),
comment_sort: cookie(&req, "comment_sort"),
}
}
// Grab a query param from a url
pub fn param(path: &str, value: &str) -> String {
let url = Url::parse(format!("https://libredd.it/{}", path).as_str()).unwrap();
let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
let pairs: HashMap<_, _> = url.query_pairs().into_owned().collect();
pairs.get(value).unwrap_or(&String::new()).to_owned()
}
// Cookies from request
// pub fn cookies(req: HttpRequest) -> HashMap<String, String> {
// let mut result: HashMap<String, String> = HashMap::new();
// let cookies: Vec<Cookie> = req
// .headers()
// .get_all("Cookie")
// .map(|value| value.to_str().unwrap())
// .map(|unparsed| Cookie::parse(unparsed).unwrap())
// .collect();
// for cookie in cookies {
// result.insert(cookie.name().to_string(), cookie.value().to_string());
// }
// result
// }
// Parse Cookie value from request
pub fn cookie(req: &HttpRequest, name: &str) -> String {
actix_web::HttpMessage::cookie(req, name).unwrap_or_else(|| Cookie::new(name, "")).value().to_string()
}
// Direct urls to proxy if proxy is enabled
pub fn format_url(url: String) -> String {
if url.is_empty() || url == "self" || url == "default" {
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
String::new()
} else {
format!("/proxy/{}", encode(url).as_str())
@ -139,15 +143,34 @@ pub fn rewrite_url(text: &str) -> String {
// Append `m` and `k` for millions and thousands respectively
pub fn format_num(num: i64) -> String {
if num > 1000000 {
format!("{}m", num / 1000000)
if num > 1_000_000 {
format!("{}m", num / 1_000_000)
} else if num > 1000 {
format!("{}k", num / 1000)
format!("{}k", num / 1_000)
} else {
num.to_string()
}
}
pub async fn media(data: &serde_json::Value) -> (String, String) {
let post_type: &str;
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
post_type = "video";
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
post_type = "video";
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default().to_string())
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
post_type = "image";
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap_or_default().to_string())
} else {
post_type = "link";
data["url"].as_str().unwrap_or_default().to_string()
};
(post_type.to_string(), url)
}
//
// JSON PARSING
//
@ -162,7 +185,7 @@ pub fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
String::from(j["data"][n][k].as_str().unwrap_or_default())
}
// Fetch posts of a user or subreddit
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
let res;
let post_list;
@ -187,12 +210,14 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
// For each post from posts list
for post in post_list {
let img = format_url(val(post, "thumbnail"));
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
let score = post["data"]["score"].as_i64().unwrap_or_default();
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
let title = val(post, "title");
// Determine the type of media along with the media URL
let (post_type, media) = media(&post["data"]).await;
posts.push(Post {
id: val(post, "id"),
title: if title.is_empty() { fallback_title.to_owned() } else { title },
@ -206,8 +231,9 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
),
score: format_num(score),
upvote_ratio: ratio as i64,
post_type: "link".to_string(),
media: img,
post_type,
thumbnail: format_url(val(post, "thumbnail")),
media,
flair: Flair(
val(post, "link_flair_text"),
val(post, "link_flair_background_color"),
@ -222,7 +248,7 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
stickied: post["data"]["stickied"].as_bool().unwrap_or_default(),
},
permalink: val(post, "permalink"),
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d '%y"), // %b %e '%y
});
}
@ -267,9 +293,9 @@ pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
}
}
// If can't send request to Reddit, return this to user
Err(e) => {
Err(_e) => {
#[cfg(debug_assertions)]
dbg!(format!("{} - {}", url, e));
dbg!(format!("{} - {}", url, _e));
Err("Couldn't send request to Reddit")
}
}

@ -2,12 +2,13 @@
:root {
--accent: aqua;
--background: #0F0F0F;
--text: white;
--foreground: #222;
--background: #0F0F0F;
--outside: #1F1F1F;
--post: #161616;
--highlighted: #333;
--black-contrast: 0 1px 3px rgba(0,0,0,0.5);
--shadow: 0 1px 3px rgba(0,0,0,0.5);
}
::selection {
@ -15,9 +16,10 @@
background: var(--accent);
}
* {
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
pre, form, fieldset, table, th, td, select, input {
margin: 0;
color: white;
color: var(--text);
font-family: sans-serif;
}
@ -35,17 +37,29 @@ nav {
padding: 5px 15px;
font-size: 20px;
min-height: 40px;
position: fixed;
width: calc(100% - 30px);
box-shadow: var(--shadow);
top: 0;
z-index: 1;
}
nav #lib, nav #github, nav #version { color: white; }
nav * { color: var(--text); }
nav #reddit { color: var(--accent); }
nav #version { opacity: 25%; }
#settings_link {
font-size: 18px;
margin-left: 20px;
opacity: 0.8;
}
main {
display: flex;
justify-content: center;
max-width: 1000px;
padding: 10px 20px;
margin: 20px auto;
margin: 60px auto 20px auto
}
#column_one {
@ -57,6 +71,7 @@ main {
footer {
display: flex;
justify-content: center;
margin-top: 20px;
}
footer > a {
@ -93,7 +108,7 @@ aside {
max-width: 350px;
}
.panel {
.post, .panel {
border: 1px solid var(--highlighted);
}
@ -177,7 +192,7 @@ aside {
}
#top > div {
border-bottom: 2px solid white;
border-bottom: 2px solid var(--text);
}
/* Sorting and Search */
@ -192,12 +207,14 @@ select, #search {
padding: 0 15px;
height: 40px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 5px 0px 0px 5px;
}
#searchbox {
display: flex;
box-shadow: var(--black-contrast);
box-shadow: var(--shadow);
}
#searchbox > *, #sort_submit {
@ -274,7 +291,7 @@ input[type="submit"]:hover { color: var(--accent); }
#sort_options, footer > a {
border-radius: 5px;
box-shadow: var(--black-contrast);
box-shadow: var(--shadow);
background: var(--outside);
display: flex;
overflow: auto;
@ -290,7 +307,7 @@ input[type="submit"]:hover { color: var(--accent); }
#sort_options > a.selected {
background: var(--accent);
color: black;
color: var(--background);
}
#sort_options > a:not(.selected):hover {
@ -299,10 +316,14 @@ input[type="submit"]:hover { color: var(--accent); }
/* Post */
.thread {
word-break: break-word;
}
.post {
border-radius: 5px;
background: var(--post);
box-shadow: var(--black-contrast);
box-shadow: var(--shadow);
display: flex;
transition: 0.2s all;
}
@ -408,7 +429,6 @@ input[type="submit"]:hover { color: var(--accent); }
.post_thumbnail {
object-fit: cover;
width: auto;
flex-shrink: 0;
border-radius: 5px;
border: 1px solid var(--foreground);
max-width: 20%;
@ -416,7 +436,7 @@ input[type="submit"]:hover { color: var(--accent); }
.post_flair {
background: var(--accent);
color: black;
color: var(--background);
padding: 5px;
border-radius: 5px;
font-size: 12px;
@ -469,7 +489,7 @@ input[type="submit"]:hover { color: var(--accent); }
.author_flair {
background: var(--highlighted);
color: white;
color: var(--text);
padding: 5px;
margin-right: 5px;
border-radius: 5px;
@ -492,7 +512,7 @@ input[type="submit"]:hover { color: var(--accent); }
.comment_right {
word-wrap: anywhere;
padding: 10px 25px 10px 5px;
padding: 10px 0 10px 5px;
flex-grow: 1;
flex-shrink: 1;
}
@ -545,15 +565,121 @@ input[type="submit"]:hover { color: var(--accent); }
background: var(--foreground);
}
.post.comment {
background: #000;
border: 2px solid var(--foreground);
/* Layouts */
#compact .post:not(.highlighted) {
border-radius: 0;
margin: 0;
padding: 0;
}
.post.comment > .post_left {
background: black;
#compact .post:first-of-type {
border-radius: 5px 5px 0 0;
overflow: hidden;
}
#compact .post:last-of-type {
border-radius: 0 0 5px 5px;
overflow: hidden;
}
#compact .post.highlighted {
border-radius: 5px;
}
#compact .post:not(:last-of-type):not(.highlighted):not(.stickied) {
border-bottom: 0;
}
#compact .post_left {
border-radius: 0;
}
#compact .post_header {
font-size: 14px;
}
#compact .post_title {
margin-top: 5px;
}
#compact .post_text {
padding: 10px;
}
#compact .post_thumbnail {
max-width: 75px;
max-height: 75px;
}
#compact footer {
margin-top: 20px;
}
#card .post_right {
flex-direction: column;
}
#card .post:not(.highlighted) .post_media {
margin-top: 0;
margin-bottom: 15px;
}
/* Settings */
#settings {
display: flex;
flex-direction: column;
align-items: center;
}
#settings_note {
font-size: 14px;
max-width: 300px;
margin-top: 10px;
opacity: 0.75;
}
#prefs {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 20px;
background: var(--post);
border-radius: 5px;
}
#prefs > div {
display: flex;
justify-content: space-between;
width: 100%;
height: 35px;
align-items: center;
}
#prefs > div:not(:last-of-type) {
margin-bottom: 10px;
}
#prefs select {
border-radius: 5px;
box-shadow: var(--shadow);
margin-left: 20px;
}
#save {
background: var(--highlighted);
padding: 10px 15px;
border-radius: 5px;
margin-top: 20px;
}
input[type="submit"] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
/* Markdown */
.md > *:not(:first-child) {
@ -573,10 +699,20 @@ input[type="submit"]:hover { color: var(--accent); }
border-left: 4px solid var(--highlighted);
}
.md a {
.md a, .md a * {
color: var(--accent);
}
.md .md-spoiler-text {
background: var(--highlighted);
color: transparent;
}
.md .md-spoiler-text:hover {
background: var(--foreground);
color: var(--text);
}
.md li { margin: 10px 0; }
.toc_child { list-style: none; }
@ -585,7 +721,7 @@ input[type="submit"]:hover { color: var(--accent); }
padding: 20px;
margin-top: 10px;
border-radius: 5px;
box-shadow: var(--black-contrast);
box-shadow: var(--shadow);
}
.md table {
@ -651,12 +787,13 @@ td, th {
main {
flex-direction: column-reverse;
padding: 10px;
margin: 10px 0;
margin: 100px 0 10px 0;
}
nav {
flex-direction: column;
padding: 10px;
width: calc(100% - 20px);
}
aside, #subreddit, #user {

@ -11,10 +11,16 @@
<link rel="stylesheet" href="/style.css">
{% endblock %}
</head>
<body>
<body id="{% block layout %}{% endblock %}">
<!-- NAVIGATION BAR -->
<nav>
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
<p id="logo">
<a id="libreddit" href="/">
<span id="lib">lib</span><span id="reddit">reddit.</span>
</a>
<span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span>
<a id="settings_link" href="/settings">settings</a>
</p>
{% block search %}{% endblock %}
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
</nav>

@ -39,7 +39,7 @@
<div id="column_one">
<!-- POST CONTENT -->
<div class="post highlighted panel">
<div class="post highlighted">
<div class="post_left">
<p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
@ -67,7 +67,7 @@
{% if post.post_type == "image" %}
<img class="post_media" src="{{ post.media }}"/>
{% else if post.post_type == "video" %}
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
<video class="post_media" src="{{ post.media }}" controls autoplay loop></video>
{% else if post.post_type == "link" %}
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
{% endif %}
@ -81,6 +81,7 @@
</ul>
<p>{{ post.upvote_ratio }}% Upvoted</p>
</div>
</div>
</div>
</div>
@ -88,7 +89,7 @@
<!-- SORT FORM -->
<form id="sort">
<select name="sort">
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select><input id="sort_submit" type="submit" value="&rarr;">
</form>

@ -3,6 +3,8 @@
{% block title %}Libreddit: search results - {{ params.q }}{% endblock %}
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
{% block content %}
<div id="column_one">
<form id="search_sort">
@ -20,8 +22,10 @@
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
</form>
{% for post in posts %}
{% if post.title != "Comment" %}
<div class="post panel">
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
{% else if post.title != "Comment" %}
<div class="post">
<div class="post_left">
<p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
@ -45,7 +49,9 @@
<a href="{{ post.permalink }}">{{ post.title }}</a>
</p>
</div>
<img class="post_thumbnail" src="{{ post.media }}">
<!-- POST THUMBNAIL -->
<img class="post_thumbnail" src="{{ post.thumbnail }}">
</div>
</div>
{% else %}

@ -9,10 +9,33 @@
{% block body %}
<main>
<form action="/settings/save" method="POST">
<label for="pref_nsfw">NSFW</label>
<input type="checkbox" name="pref_nsfw" id="pref_nsfw" {% if pref_nsfw == "on" %}checked{% endif %}>
<input id="sort_submit" type="submit" value="&rarr;">
<form id="settings" action="/settings" method="POST">
<div id="prefs">
<div id="front_page">
<label for="front_page">Front page:</label>
<select name="front_page">
{% call utils::options(prefs.front_page, ["popular", "all"], "popular") %}
</select>
</div>
<div id="layout">
<label for="layout">Layout:</label>
<select name="layout">
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "clean") %}
</select>
</div>
<div id="comment_sort">
<label for="comment_sort">Default comment sort:</label>
<select name="comment_sort">
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
</select>
</div>
<div id="hide_nsfw">
<label for="hide_nsfw">Hide NSFW posts:</label>
<input type="checkbox" name="hide_nsfw" {% if prefs.hide_nsfw == "on" %}checked{% endif %}>
</div>
</div>
<p id="settings_note"><b>Note:</b> settings are saved in browser cookies. Clearing your cookie data will reset them.</p>
<input id="save" type="submit" value="Save">
</form>
</main>
{% endblock %}

@ -11,6 +11,8 @@
{% call utils::search(["/r/", sub.name.as_str()].concat(), "") %}
{% endblock %}
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
{% block body %}
<main>
<div id="column_one">
@ -27,8 +29,11 @@
<input id="sort_submit" type="submit" value="&rarr;">
</select>{% endif %}
</form>
<div id="posts">
{% for post in posts %}
<div class="post {% if post.flags.stickied %}stickied{% endif %} panel">
{% if !(post.flags.nsfw && prefs.hide_nsfw == "on") %}
<div class="post {% if post.flags.stickied %}stickied{% endif %}">
<div class="post_left">
<p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
@ -49,10 +54,18 @@
<a href="{{ post.permalink }}">{{ post.title }}</a>
</p>
</div>
<img class="post_thumbnail" src="{{ post.media }}">
<!-- POST MEDIA/THUMBNAIL -->
{% if prefs.layout == "card" && post.post_type == "image" %}
<img class="post_media" src="{{ post.media }}"/>
{% else if prefs.layout != "card" %}
<img class="post_thumbnail" src="{{ post.thumbnail }}">
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<footer>
{% if ends.0 != "" %}

@ -6,6 +6,9 @@
{% endblock %}
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Libreddit{% endblock %}
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
{% block body %}
<main style="max-width: 1000px;">
<div id="column_one">
@ -16,9 +19,13 @@
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
</form>
<div id="posts">
{% for post in posts %}
{% if post.title != "Comment" %}
<div class="post panel">
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
{% else if post.title != "Comment" %}
<div class="post">
<div class="post_left">
<p class="post_score">{{ post.score }}</p>
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
@ -31,7 +38,7 @@
<small class="author_flair">{{ post.author_flair.0 }}</small>
{% endif %}
<span class="dot">&bull;</span>
<span class="datetime" style="float: right;">{{ post.time }}</span>
<span class="datetime">{{ post.time }}</span>
</p>
<p class="post_title">
{% if post.flair.0 == "Comment" %}
@ -42,7 +49,13 @@
<a href="{{ post.permalink }}">{{ post.title }}</a>
</p>
</div>
<img class="post_thumbnail" src="{{ post.media }}">
<!-- POST MEDIA/THUMBNAIL -->
{% if prefs.layout == "card" && post.post_type == "image" %}
<img class="post_media" src="{{ post.media }}"/>
{% else if prefs.layout != "card" %}
<img class="post_thumbnail" src="{{ post.thumbnail }}">
{% endif %}
</div>
</div>
{% else %}
@ -60,7 +73,10 @@
</details>
</div>
{% endif %}
{% endfor %}
</div>
<footer>
{% if ends.0 != "" %}
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>