Api & JS client

This commit is contained in:
Pascal Engélibert 2023-01-11 22:56:32 +01:00
parent 8629bba07f
commit 5deba2fdb1
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
10 changed files with 399 additions and 144 deletions

99
Cargo.lock generated
View file

@ -304,9 +304,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.60" version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -353,9 +353,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.20.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
@ -421,11 +421,12 @@ dependencies = [
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.17" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde",
] ]
[[package]] [[package]]
@ -681,9 +682,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-flags", "cxxbridge-flags",
@ -693,9 +694,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
@ -708,15 +709,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -768,7 +769,7 @@ dependencies = [
"hashbrown", "hashbrown",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core 0.9.5", "parking_lot_core 0.9.6",
] ]
[[package]] [[package]]
@ -1149,9 +1150,9 @@ dependencies = [
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.9" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
@ -1399,11 +1400,10 @@ dependencies = [
[[package]] [[package]]
name = "ignore" name = "ignore"
version = "0.4.18" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3"
dependencies = [ dependencies = [
"crossbeam-utils",
"globset", "globset",
"lazy_static", "lazy_static",
"log", "log",
@ -1462,9 +1462,9 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.7.0" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]] [[package]]
name = "itertools" name = "itertools"
@ -1770,9 +1770,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.5" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -1809,9 +1809,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a"
dependencies = [ dependencies = [
"thiserror", "thiserror",
"ucd-trie", "ucd-trie",
@ -1819,9 +1819,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_derive" name = "pest_derive"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6"
dependencies = [ dependencies = [
"pest", "pest",
"pest_generator", "pest_generator",
@ -1829,9 +1829,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_generator" name = "pest_generator"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
@ -1842,13 +1842,13 @@ dependencies = [
[[package]] [[package]]
name = "pest_meta" name = "pest_meta"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"pest", "pest",
"sha1 0.10.5", "sha2 0.10.6",
] ]
[[package]] [[package]]
@ -2127,9 +2127,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.7.0" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2349,11 +2349,11 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.21.0",
] ]
[[package]] [[package]]
@ -2486,17 +2486,6 @@ dependencies = [
"sha1_smol", "sha1_smol",
] ]
[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.6",
]
[[package]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.0" version = "1.0.0"
@ -2665,7 +2654,7 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"sha1 0.6.1", "sha1",
"syn", "syn",
] ]
@ -2865,9 +2854,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.23.0" version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -2988,9 +2977,9 @@ dependencies = [
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]] [[package]]
name = "type-map" name = "type-map"
@ -3355,7 +3344,7 @@ name = "webcomment"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"base64 0.20.0", "base64 0.21.0",
"clap", "clap",
"crossbeam-channel", "crossbeam-channel",
"directories", "directories",

View file

@ -9,7 +9,7 @@ edition = "2021"
[dependencies] [dependencies]
argon2 = "0.4.1" argon2 = "0.4.1"
base64 = "0.20.0" base64 = "0.21.0"
clap = { version = "4.0.32", default-features = false, features = ["derive", "error-context", "help", "std", "usage"] } clap = { version = "4.0.32", default-features = false, features = ["derive", "error-context", "help", "std", "usage"] }
crossbeam-channel = "0.5.6" crossbeam-channel = "0.5.6"
directories = "4.0.1" directories = "4.0.1"
@ -29,7 +29,7 @@ sha2 = "0.10.6"
sled = "0.34.7" sled = "0.34.7"
tera = { version = "1.17.1", features = ["builtins", "date-locale"] } tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] } tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
tokio = { version = "1.23.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.24.1", features = ["macros", "rt-multi-thread"] }
toml_edit = { version = "0.17.1", features = ["easy"] } toml_edit = { version = "0.17.1", features = ["easy"] }
typed-sled = "0.2.3" typed-sled = "0.2.3"
unic-langid = { version = "0.9.1", features = ["macros"] } unic-langid = { version = "0.9.1", features = ["macros"] }

View file

@ -5,6 +5,7 @@
<title>Webcomment</title> <title>Webcomment</title>
<script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/webcomment.js"></script> <script type="text/javascript" src="js/webcomment.js"></script>
<link rel="stylesheet" href="style.css"/>
</head> </head>
<body> <body>
<div id="comments"></div> <div id="comments"></div>

View file

@ -8,27 +8,68 @@ You should have received a copy of the GNU Affero General Public License along w
var webcomments = {}; var webcomments = {};
const MODE_TOPIC = 1;// param: {topic:str} const MODE_TOPIC = 1;// param: {topic:str}
const ORDER_BY_DATE_ASC = 1;
const ORDER_BY_DATE_DESC = 2;
const DEFAULT_CONFIG = {
default_order: ORDER_BY_DATE_ASC,
/*template_comment: `
<div class="comment" id="{{root.id}}-{{comment.id}}">
<p class="comment-meta">{{comment.author}} {{comment.post_time}}</p>
<p class="comment-text">{{comment.text}}</p>
</div>`,*/
template_comment: `
<div class="comment" id="{{root.id}}-{{comment.id}}">
<p class="comment-meta">
<a class="comment-post_time" aria-label="Permalink" title="Permalink" href="#{{root.id}}-{{comment.id}}">{{comment.post_time}}</a>
<span class="comment-author"></span></p>
<p class="comment-text"></p>
</div>`,
template_widget: `
<div class="comments"></div>
<form class="comment-new-form" action="#" onsubmit="event.preventDefault();post_new_comment('{{root.id}}')">
<fieldset>
<legend>New comment</legend>
<label>
Your name:
<input type="text" name="author" class="comment-form-author"/>
</label><br/>
<label>
Your email:
<input type="email" name="email" class="comment-form-email"/>
</label><br/>
<textarea class="comment-form-text" name="text"></textarea><br/>
<input type="submit" value="Post"/>
</fieldset>
</form>
`,
};
class Webcomment { class Webcomment {
constructor(root, api, mode, mode_param) { constructor(root_id, api, mode, mode_param, config) {
this.root = root; this.root_id = root_id;
this.api = api; this.api = api;
this.mode = mode; this.mode = mode;
this.mode_param = mode_param; this.mode_param = mode_param;
this.config = config;
console.log("constr"); this.root = document.getElementById(root_id);
this.root.innerHTML = config.template_widget.replaceAll("{{root.id}}", this.root_id);;
this.elem_comments = this.root.getElementsByClassName("comments")[0];
switch(mode) { switch(mode) {
case MODE_TOPIC: case MODE_TOPIC:
this.query_comments_by_topic(mode_param.topic); var this_ = this;
this.query_comments_by_topic(mode_param.topic, function(resp) {
this_.append_comments(resp.comments);
});
break; break;
default: default:
console.log("Webcomment: invalid mode"); console.log("Webcomment: invalid mode");
} }
} }
query_comments_by_topic(topic) { query_comments_by_topic(topic, success) {
console.log("query");
$.ajax({ $.ajax({
method: "POST", method: "POST",
url: this.api+"/api/comments_by_topic", url: this.api+"/api/comments_by_topic",
@ -36,15 +77,80 @@ class Webcomment {
mutation_token: "", mutation_token: "",
topic: topic, topic: topic,
}), }),
success: function(resp) { success: success,
console.log(resp);
},
dataType: "json", dataType: "json",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
}); });
} }
query_new_comment(topic, author, email, text, success) {
$.ajax({
method: "POST",
url: this.api+"/api/new_comment",
data: JSON.stringify({
author: author,
email: email,
text: text,
topic: topic,
}),
success: success,
dataType: "json",
contentType: "application/json; charset=utf-8",
});
}
post_new_comment() {
var elem_author = $("#comments .comment-new-form [name=author]")[0];
var elem_email = $("#comments .comment-new-form [name=email]")[0];
var elem_text = $("#comments .comment-new-form [name=text]")[0];
switch(this.mode) {
case MODE_TOPIC:
var comment = {
topic: this.mode_param.topic,
author: elem_author.value,
email: elem_email.value,
text: elem_text.value,
};
var this_ = this;
this.query_new_comment(comment.topic, comment.author, comment.email, comment.text, function(resp) {
if(resp.id) {
comment.id = resp.id;
comment.post_time = resp.post_time;
this_.append_comments([comment]);
elem_text.value = "";
}
});
break;
default:
console.log("Webcomment: invalid mode");
}
}
append_comments(comments) {
for(var i in comments) {
var comment = comments[i];
var post_time = new Date(comment.post_time*1000);
var comment_html = this.config.template_comment;
comment_html = comment_html.replaceAll("{{root.id}}", this.root_id);
comment_html = comment_html.replaceAll("{{comment.id}}", comment.id);
//comment_html = comment_html.replaceAll("{{comment.author}}", comment.author);
comment_html = comment_html.replaceAll("{{comment.post_time}}", post_time.toLocaleDateString()+" "+post_time.toLocaleTimeString());
//comment_html = comment_html.replaceAll("{{comment.text}}", comment.text);
$(this.elem_comments).append(comment_html);
var elem = document.getElementById(this.root_id+"-"+comment.id);
elem.getElementsByClassName("comment-author")[0].innerHTML = comment.author;
elem.getElementsByClassName("comment-text")[0].innerHTML = comment.text;
}
}
} }
function webcomment_topic(root_id, api, topic) { function webcomment_topic(root_id, api, topic, config=DEFAULT_CONFIG) {
webcomments[root_id] = (new Webcomment(document.getElementById(root_id), api, MODE_TOPIC, {topic: topic})); webcomments[root_id] = (new Webcomment(root_id, api, MODE_TOPIC, {topic: topic}, config));
}
function post_new_comment(root_id) {
webcomments[root_id].post_new_comment();
} }

9
client/style.css Normal file
View file

@ -0,0 +1,9 @@
.comment {
border-left: 2px solid #888;
border-right: 2px solid #888;
padding: 4px;
}
.comment-meta, .comment-meta a, .comment-meta a:visited {
color: #666;
}

View file

@ -12,10 +12,10 @@ const DB_DIR: &str = "db";
pub type Time = u64; pub type Time = u64;
pub const BASE64: base64::engine::fast_portable::FastPortable = pub const BASE64: base64::engine::general_purpose::GeneralPurpose =
base64::engine::fast_portable::FastPortable::from( base64::engine::general_purpose::GeneralPurpose::new(
&base64::alphabet::URL_SAFE, &base64::alphabet::URL_SAFE,
base64::engine::fast_portable::NO_PAD, base64::engine::general_purpose::NO_PAD,
); );
#[derive(Clone)] #[derive(Clone)]
@ -74,20 +74,13 @@ impl MutationToken {
} }
pub fn to_base64(&self) -> String { pub fn to_base64(&self) -> String {
let mut buf = vec![0; 24]; BASE64.encode(self.0)
let size = BASE64.encode(&self.0, &mut buf);
buf.truncate(size);
String::from_utf8(buf).unwrap()
} }
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> { pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
std::panic::catch_unwind(|| { std::panic::catch_unwind(|| {
let mut buf = [0; 16]; let mut buf = [0; 16];
BASE64.decode( BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
s.as_bytes(),
&mut buf,
BASE64.decoded_length_estimate(s.len()),
)?;
Ok(Self(buf)) Ok(Self(buf))
}) })
.map_err(|_| base64::DecodeError::InvalidLength)? .map_err(|_| base64::DecodeError::InvalidLength)?
@ -134,20 +127,13 @@ impl CommentId {
} }
pub fn to_base64(&self) -> String { pub fn to_base64(&self) -> String {
let mut buf = vec![0; 24]; BASE64.encode(self.0)
let size = BASE64.encode(&self.0, &mut buf);
buf.truncate(size);
String::from_utf8(buf).unwrap()
} }
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> { pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
std::panic::catch_unwind(|| { std::panic::catch_unwind(|| {
let mut buf = [0; 16]; let mut buf = [0; 16];
BASE64.decode( BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
s.as_bytes(),
&mut buf,
BASE64.decoded_length_estimate(s.len()),
)?;
Ok(Self(buf)) Ok(Self(buf))
}) })
.map_err(|_| base64::DecodeError::InvalidLength)? .map_err(|_| base64::DecodeError::InvalidLength)?

View file

@ -29,7 +29,7 @@ pub async fn run_server(
.build()) .build())
}); });
api::init_routes(&mut app, config, dbs.clone()).await; api::init_routes(&mut app, config, dbs.clone(), notify_send.clone()).await;
page::init_routes(&mut app, config, dbs, templates, locales, notify_send).await; page::init_routes(&mut app, config, dbs, templates, locales, notify_send).await;
app.listen(config.listen).await.unwrap(); app.listen(config.listen).await.unwrap();

View file

@ -1,54 +1,29 @@
#![allow(clippy::too_many_arguments)] mod queries;
mod resps;
use crate::{config::*, db::*, helpers, notify::Notification}; use crate::{config::*, db::*, helpers, notify::Notification};
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use log::{error, warn}; use log::{error, warn};
use serde::{Deserialize, Serialize};
enum ApiError { pub async fn init_routes(
InvalidAdminPassword, app: &mut tide::Server<()>,
} config: &'static Config,
dbs: Dbs,
pub async fn init_routes(app: &mut tide::Server<()>, config: &'static Config, dbs: Dbs) { notify_send: Sender<Notification>,
) {
// TODO pagination // TODO pagination
app.at(&format!("{}api/comments_by_topic", config.root_url)) app.at(&format!("{}api/comments_by_topic", config.root_url))
.post({ .post({
let dbs = dbs.clone(); let dbs = dbs.clone();
move |req: tide::Request<()>| query_comments_by_topic(req, config, dbs.clone()) move |req: tide::Request<()>| query_comments_by_topic(req, config, dbs.clone())
}); });
} app.at(&format!("{}api/new_comment", config.root_url))
.post({
#[derive(Serialize)] move |req: tide::Request<()>| {
struct CommentWithId { query_new_comment(req, config, dbs.clone(), notify_send.clone())
pub addr: Option<String>, }
pub author: String, });
pub editable: bool,
pub id: String,
pub last_edit_time: Option<Time>,
pub status: Option<OriginalComment>,
pub post_time: Time,
pub text: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct OriginalComment {
pub author: String,
pub editable: bool,
pub last_edit_time: Option<Time>,
pub post_time: Time,
pub text: String,
}
#[derive(Deserialize)]
struct CommentsByTopicQuery {
mutation_token: Option<String>,
topic: String,
}
#[derive(Serialize)]
struct CommentsByTopicResp {
comments: Vec<CommentWithId>,
} }
async fn query_comments_by_topic( async fn query_comments_by_topic(
@ -56,11 +31,17 @@ async fn query_comments_by_topic(
config: &Config, config: &Config,
dbs: Dbs, dbs: Dbs,
) -> tide::Result<tide::Response> { ) -> tide::Result<tide::Response> {
let Ok(CommentsByTopicQuery { let Ok(queries::CommentsByTopic {
mutation_token, mutation_token: _mutation_token,
topic, topic,
}) = req.body_json().await else { }) = req.body_json().await else {
return Err(tide::Error::from_str(400, "Invalid request")); return Ok(tide::Response::builder(400)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(
tide::Body::from_json(&resps::Error::InvalidRequest).unwrap(),
)
.build());
}; };
let topic_hash = TopicHash::from_topic(&topic); let topic_hash = TopicHash::from_topic(&topic);
@ -69,19 +50,21 @@ async fn query_comments_by_topic(
.content_type(tide::http::mime::JSON) .content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin) .header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body( .body(
tide::Body::from_json(&CommentsByTopicResp { tide::Body::from_json(&resps::CommentsByTopic {
comments: helpers::iter_approved_comments_by_topic(topic_hash, &dbs) comments: helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
.map(|(comment_id, comment, _comment_status)| CommentWithId { .map(
addr: None, |(comment_id, comment, _comment_status)| resps::CommentWithId {
author: comment.author, addr: None,
editable: false, author: comment.author,
id: comment_id.to_base64(), editable: false,
last_edit_time: comment.last_edit_time, id: comment_id.to_base64(),
post_time: comment.post_time, last_edit_time: comment.last_edit_time,
status: None, post_time: comment.post_time,
text: comment.text, status: None,
}) text: comment.text,
.collect::<Vec<CommentWithId>>(), },
)
.collect::<Vec<resps::CommentWithId>>(),
}) })
.map_err(|e| { .map_err(|e| {
error!("Serializing CommentsByTopicResp to json: {e:?}"); error!("Serializing CommentsByTopicResp to json: {e:?}");
@ -90,3 +73,125 @@ async fn query_comments_by_topic(
) )
.build()) .build())
} }
async fn query_new_comment(
mut req: tide::Request<()>,
config: &Config,
dbs: Dbs,
notify_send: Sender<Notification>,
) -> tide::Result<tide::Response> {
let Ok(query) = req.body_json::<queries::NewComment>().await else {
return Ok(tide::Response::builder(400)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(
tide::Body::from_json(&resps::Error::InvalidRequest).unwrap(),
)
.build());
};
if query.author.len() > config.comment_author_max_len
|| query.email.len() > config.comment_email_max_len
|| query.text.len() > config.comment_text_max_len
{
return Ok(tide::Response::builder(400)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(tide::Body::from_json(&resps::Error::IllegalContent).unwrap())
.build());
}
let client_addr = match helpers::get_client_addr(config, &req) {
Some(Ok(addr)) => Some(addr),
Some(Err(e)) => {
warn!("Unable to parse client addr: {}", e);
None
}
None => {
warn!("No client addr");
None
}
};
let antispam_enabled = config.antispam_enable
&& client_addr
.as_ref()
.map_or(false, |addr| !config.antispam_whitelist.contains(addr));
if let Some(client_addr) = &client_addr {
if antispam_enabled {
if let Some(antispam_timeout) =
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
{
return Ok(tide::Response::builder(403)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(
tide::Body::from_json(&resps::Error::Antispam {
timeout: antispam_timeout,
})
.unwrap(),
)
.build());
}
}
}
// It's OK
if let Some(client_addr) = &client_addr {
if antispam_enabled {
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
}
}
let topic_hash = TopicHash::from_topic(&query.topic);
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let comment = Comment {
topic_hash,
author: if query.author.is_empty() {
petname::Petnames::large().generate_one(2, " ")
} else {
query.author
},
email: if query.email.is_empty() {
None
} else {
Some(query.email)
},
last_edit_time: None,
mutation_token: MutationToken::new(),
post_time: time,
text: query.text,
};
match helpers::new_pending_comment(&comment, client_addr, &dbs) {
Ok(comment_id) => {
notify_send.send(Notification { topic: query.topic }).ok();
Ok(tide::Response::builder(200)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(
tide::Body::from_json(&resps::NewComment {
id: comment_id.to_base64(),
mutation_token: comment.mutation_token.to_base64(),
post_time: time,
})
.unwrap(),
)
.build())
}
// TODO add message to client log and change http code
Err(e) => {
error!("Adding pending comment: {:?}", e);
Ok(tide::Response::builder(500)
.content_type(tide::http::mime::JSON)
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
.body(tide::Body::from_json(&resps::Error::Internal).unwrap())
.build())
}
}
}

15
src/server/api/queries.rs Normal file
View file

@ -0,0 +1,15 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommentsByTopic {
pub mutation_token: Option<String>,
pub topic: String,
}
#[derive(Deserialize)]
pub struct NewComment {
pub author: String,
pub email: String,
pub text: String,
pub topic: String,
}

44
src/server/api/resps.rs Normal file
View file

@ -0,0 +1,44 @@
use crate::db::*;
use serde::Serialize;
#[derive(Serialize)]
pub enum Error {
Antispam { timeout: Time },
IllegalContent,
Internal,
InvalidRequest,
}
#[derive(Serialize)]
pub struct CommentsByTopic {
pub comments: Vec<CommentWithId>,
}
#[derive(Clone, Debug, Serialize)]
pub struct OriginalComment {
pub author: String,
pub editable: bool,
pub last_edit_time: Option<Time>,
pub post_time: Time,
pub text: String,
}
#[derive(Serialize)]
pub struct CommentWithId {
pub addr: Option<String>,
pub author: String,
pub editable: bool,
pub id: String,
pub last_edit_time: Option<Time>,
pub status: Option<OriginalComment>,
pub post_time: Time,
pub text: String,
}
#[derive(Serialize)]
pub struct NewComment {
pub id: String,
pub mutation_token: String,
pub post_time: Time,
}