diff --git a/Cargo.lock b/Cargo.lock
index 6578d17..a29b750 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -71,12 +71,6 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
-[[package]]
-name = "anyhow"
-version = "1.0.93"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
-
[[package]]
name = "arc-swap"
version = "1.7.1"
@@ -139,6 +133,12 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "base2048"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71f4fe417e8cc3bb9b437dfa9290ce92bd2730ba5374719bdfd9147fbc8f17cd"
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -151,6 +151,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "bitflags"
version = "2.6.0"
@@ -319,18 +328,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
-[[package]]
-name = "common-words-all"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84a6ff47eb813c9e315610ceca0ddd247827e22f2cdadc4189e4676a81470c77"
-dependencies = [
- "anyhow",
- "csv",
- "glob",
- "serde",
-]
-
[[package]]
name = "cookie"
version = "0.18.1"
@@ -394,27 +391,6 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "csv"
-version = "1.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
-dependencies = [
- "csv-core",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "csv-core"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "darling"
version = "0.20.10"
@@ -698,12 +674,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
-
[[package]]
name = "globset"
version = "0.4.15"
@@ -1381,13 +1351,14 @@ version = "0.35.1"
dependencies = [
"arc-swap",
"async-recursion",
+ "base2048",
"base64 0.22.1",
+ "bincode",
"brotli",
"build_html",
"cached",
"chrono",
"clap",
- "common-words-all",
"cookie",
"dotenvy",
"fastrand",
@@ -1403,6 +1374,7 @@ dependencies = [
"pretty_env_logger",
"pulldown-cmark",
"regex",
+ "revision",
"rinja",
"route-recognizer",
"rss",
@@ -1459,6 +1431,26 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+[[package]]
+name = "revision"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c"
+dependencies = [
+ "revision-derive",
+]
+
+[[package]]
+name = "revision-derive"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "ring"
version = "0.17.8"
diff --git a/Cargo.toml b/Cargo.toml
index 759b148..c7b6d4a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -48,12 +48,14 @@ arc-swap = "1.7.1"
serde_json_path = "0.7.1"
async-recursion = "1.1.1"
pulldown-cmark = { version = "0.12.0", features = ["simd", "html"], default-features = false }
-common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] }
hyper-rustls = { version = "0.24.2", features = [ "http2" ] }
tegen = "0.1.4"
serde_urlencoded = "0.7.1"
chrono = { version = "0.4.39", default-features = false, features = [ "std" ] }
htmlescape = "0.3.1"
+bincode = "1.3.3"
+base2048 = "2.0.2"
+revision = "0.10.0"
[dev-dependencies]
diff --git a/src/main.rs b/src/main.rs
index 165f0cb..bcfcd51 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -246,6 +246,7 @@ async fn main() {
app
.at("/check_update.js")
.get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed());
+ app.at("/copy.js").get(|_| resource(include_str!("../static/copy.js"), "text/javascript", false).boxed());
app.at("/commits.atom").get(|_| async move { proxy_commit_info().await }.boxed());
app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed());
@@ -284,6 +285,7 @@ async fn main() {
// Configure settings
app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed());
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
+ app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed());
// RSS Subscriptions
diff --git a/src/settings.rs b/src/settings.rs
index 4b30da3..6649f69 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -5,12 +5,13 @@ use std::collections::HashMap;
// CRATES
use crate::server::ResponseExt;
use crate::subreddit::join_until_size_limit;
-use crate::utils::{redirect, template, Preferences};
+use crate::utils::{deflate_decompress, redirect, template, Preferences};
use cookie::Cookie;
use futures_lite::StreamExt;
use hyper::{Body, Request, Response};
use rinja::Template;
use time::{Duration, OffsetDateTime};
+use url::form_urlencoded;
// STRUCTS
#[derive(Template)]
@@ -262,3 +263,25 @@ pub async fn restore(req: Request
) -> Result, String> {
pub async fn update(req: Request) -> Result, String> {
Ok(set_cookies_method(req, false))
}
+
+pub async fn encoded_restore(req: Request) -> Result, String> {
+ let body = hyper::body::to_bytes(req.into_body())
+ .await
+ .map_err(|e| format!("Failed to get bytes from request body: {}", e))?;
+
+ let encoded_prefs = form_urlencoded::parse(&body)
+ .find(|(key, _)| key == "encoded_prefs")
+ .map(|(_, value)| value)
+ .ok_or_else(|| "encoded_prefs parameter not found in request body".to_string())?;
+
+ let bytes = base2048::decode(&encoded_prefs).ok_or_else(|| "Failed to decode base2048 encoded preferences".to_string())?;
+
+ let out = deflate_decompress(bytes)?;
+
+ let mut prefs: Preferences = bincode::deserialize(&out).map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?;
+ prefs.available_themes = vec![];
+
+ let url = format!("/settings/restore/?{}", prefs.to_urlencoded()?);
+
+ Ok(redirect(&url))
+}
diff --git a/src/utils.rs b/src/utils.rs
index 2747449..c4ab679 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -9,16 +9,19 @@ use crate::{client::json, server::RequestExt};
use cookie::Cookie;
use htmlescape::decode_html;
use hyper::{Body, Request, Response};
+use libflate::deflate::{Decoder, Encoder};
use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
+use revision::revisioned;
use rinja::Template;
use rust_embed::RustEmbed;
-use serde::{Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet};
use std::env;
+use std::io::{Read, Write};
use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime};
@@ -618,32 +621,55 @@ pub struct Params {
pub before: Option,
}
-#[derive(Default, Serialize)]
+#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
+#[revisioned(revision = 1)]
pub struct Preferences {
- #[serde(skip)]
+ #[revision(start = 1)]
+ #[serde(skip_serializing, skip_deserializing)]
pub available_themes: Vec,
+ #[revision(start = 1)]
pub theme: String,
+ #[revision(start = 1)]
pub front_page: String,
+ #[revision(start = 1)]
pub layout: String,
+ #[revision(start = 1)]
pub wide: String,
+ #[revision(start = 1)]
pub blur_spoiler: String,
+ #[revision(start = 1)]
pub show_nsfw: String,
+ #[revision(start = 1)]
pub blur_nsfw: String,
+ #[revision(start = 1)]
pub hide_hls_notification: String,
+ #[revision(start = 1)]
pub video_quality: String,
+ #[revision(start = 1)]
pub hide_sidebar_and_summary: String,
+ #[revision(start = 1)]
pub use_hls: String,
+ #[revision(start = 1)]
pub autoplay_videos: String,
+ #[revision(start = 1)]
pub fixed_navbar: String,
+ #[revision(start = 1)]
pub disable_visit_reddit_confirmation: String,
+ #[revision(start = 1)]
pub comment_sort: String,
+ #[revision(start = 1)]
pub post_sort: String,
- #[serde(serialize_with = "serialize_vec_with_plus")]
+ #[revision(start = 1)]
+ #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")]
pub subscriptions: Vec,
- #[serde(serialize_with = "serialize_vec_with_plus")]
+ #[revision(start = 1)]
+ #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")]
pub filters: Vec,
+ #[revision(start = 1)]
pub hide_awards: String,
+ #[revision(start = 1)]
pub hide_score: String,
+ #[revision(start = 1)]
pub remove_default_feeds: String,
}
@@ -654,6 +680,17 @@ where
serializer.serialize_str(&vec.join("+"))
}
+fn deserialize_vec_with_plus<'de, D>(deserializer: D) -> Result, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ let string = String::deserialize(deserializer)?;
+ if string.is_empty() {
+ return Ok(Vec::new());
+ }
+ Ok(string.split('+').map(|s| s.to_string()).collect())
+}
+
#[derive(RustEmbed)]
#[folder = "static/themes/"]
#[include = "*.css"]
@@ -698,6 +735,29 @@ impl Preferences {
pub fn to_urlencoded(&self) -> Result {
serde_urlencoded::to_string(self).map_err(|e| e.to_string())
}
+
+ pub fn to_bincode(&self) -> Result, String> {
+ bincode::serialize(self).map_err(|e| e.to_string())
+ }
+ pub fn to_compressed_bincode(&self) -> Result, String> {
+ deflate_compress(self.to_bincode()?)
+ }
+ pub fn to_bincode_str(&self) -> Result {
+ Ok(base2048::encode(&self.to_compressed_bincode()?))
+ }
+}
+
+pub fn deflate_compress(i: Vec) -> Result, String> {
+ let mut e = Encoder::new(Vec::new());
+ e.write_all(&i).map_err(|e| e.to_string())?;
+ e.finish().into_result().map_err(|e| e.to_string())
+}
+
+pub fn deflate_decompress(i: Vec) -> Result, String> {
+ let mut decoder = Decoder::new(&i[..]);
+ let mut out = Vec::new();
+ decoder.read_to_end(&mut out).map_err(|e| format!("Failed to read from gzip decoder: {}", e))?;
+ Ok(out)
}
/// Gets a `HashSet` of filters from the cookie in the given `Request`.
@@ -1584,3 +1644,52 @@ How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or t
assert_eq!(render_bullet_lists(input), output);
}
+
+#[test]
+fn test_default_prefs_serialization_loop_json() {
+ let prefs = Preferences::default();
+ let serialized = serde_json::to_string(&prefs).unwrap();
+ let deserialized: Preferences = serde_json::from_str(&serialized).unwrap();
+ assert_eq!(prefs, deserialized);
+}
+
+#[test]
+fn test_default_prefs_serialization_loop_bincode() {
+ let prefs = Preferences::default();
+ test_round_trip(&prefs, false);
+ test_round_trip(&prefs, true);
+}
+
+static KNOWN_GOOD_CONFIGS: &[&str] = &[
+ "ఴӅβØØҞÉဏႢձĬ༧ȒʯऌԔӵ୮༏",
+ "ਧՊΥÀÃǎƱГ۸ඣമĖฤ႙ʟาúໜϾௐɥঀĜໃહཞઠѫҲɂఙ࿔DzઉƲӟӻĻฅΜδ໖ԜǗဖငƦơ৶Ą௩ԹʛใЛʃශаΏ",
+ "ਧԩΥÀÃΊ౭൩ඔႠϼҭöҪƸռઇԾॐნɔາǒՍҰच௨ಖມŃЉŐདƦ๙ϩএఠȝഽйʮჯඒϰळՋ௮ສ৵ऎΦѧਹಧଟƙŃ३î༦ŌပղयƟแҜ།",
+];
+
+#[test]
+fn test_known_good_configs_deserialization() {
+ for config in KNOWN_GOOD_CONFIGS {
+ let bytes = base2048::decode(config).unwrap();
+ let decompressed = deflate_decompress(bytes).unwrap();
+ assert!(bincode::deserialize::(&decompressed).is_ok());
+ }
+}
+
+#[test]
+fn test_known_good_configs_full_round_trip() {
+ for config in KNOWN_GOOD_CONFIGS {
+ let bytes = base2048::decode(config).unwrap();
+ let decompressed = deflate_decompress(bytes).unwrap();
+ let prefs: Preferences = bincode::deserialize(&decompressed).unwrap();
+ test_round_trip(&prefs, false);
+ test_round_trip(&prefs, true);
+ }
+}
+
+fn test_round_trip(input: &Preferences, compression: bool) {
+ let serialized = bincode::serialize(input).unwrap();
+ let compressed = if compression { deflate_compress(serialized).unwrap() } else { serialized };
+ let decompressed = if compression { deflate_decompress(compressed).unwrap() } else { compressed };
+ let deserialized: Preferences = bincode::deserialize(&decompressed).unwrap();
+ assert_eq!(*input, deserialized);
+}
diff --git a/static/copy.js b/static/copy.js
new file mode 100644
index 0000000..d7c7dd4
--- /dev/null
+++ b/static/copy.js
@@ -0,0 +1,9 @@
+async function copy() {
+ await navigator.clipboard.writeText(document.getElementById('bincode_str').value);
+}
+
+async function set_listener() {
+ document.getElementById('copy').addEventListener('click', copy);
+}
+
+window.addEventListener('load', set_listener);
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
index 76b55dd..545567e 100644
--- a/static/style.css
+++ b/static/style.css
@@ -553,14 +553,18 @@ aside {
.subscribe,
.unsubscribe,
.filter,
-.unfilter {
+.unfilter,
+.copy,
+.import {
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
.subscribe,
-.filter {
+.filter,
+.copy,
+.import {
color: var(--foreground);
background-color: var(--accent);
}
diff --git a/templates/settings.html b/templates/settings.html
index 7995312..c3d8086 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -4,22 +4,22 @@
{% block title %}Redlib Settings{% endblock %}
{% block subscriptions %}
- {% call utils::sub_list("") %}
+{% call utils::sub_list("") %}
{% endblock %}
{% block search %}
- {% call utils::search("".to_owned(), "") %}
+{% call utils::search("".to_owned(), "") %}
{% endblock %}
{% block content %}
-
+
{% if prefs.subscriptions.len() > 0 %}
-
-
- {% for sub in prefs.subscriptions %}
-
- {% let feed -%}
- {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%}
-
{{ feed }}
-
-
- {% endfor %}
+
+
+ {% for sub in prefs.subscriptions %}
+
+ {% let feed -%}
+ {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed =
+ format!("r/{}", sub) -%}{% endif -%}
+
{{ feed }}
+
+ {% endfor %}
+
{% endif %}
{% if !prefs.filters.is_empty() %}
-
-
- {% for sub in prefs.filters %}
-
- {% let feed -%}
- {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%}
-
{{ feed }}
-
-
- {% endfor %}
+
+
+ {% for sub in prefs.filters %}
+
+ {% let feed -%}
+ {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed =
+ format!("r/{}", sub) -%}{% endif -%}
+
{{ feed }}
+
+ {% endfor %}
+
{% endif %}
-
Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.
+
Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.
+
{% match prefs.to_urlencoded() %}
{% when Ok with (encoded_prefs) %}
@@ -176,7 +194,24 @@
There was an error creating your restore link: {{ err }}
Please report this issue
{% endmatch %}
+
+
+
+
+
+
+
+
+
+
+
-{% endblock %}
+{% endblock %}
\ No newline at end of file