wip
This commit is contained in:
parent
5deba2fdb1
commit
4e8f84480d
48 changed files with 6372 additions and 1209 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
/webui/dist
|
||||||
|
|
|
||||||
1999
Cargo.lock
generated
1999
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
45
Cargo.toml
45
Cargo.toml
|
|
@ -1,38 +1,7 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "webcomment"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
members = [
|
||||||
authors = ["tuxmain <tuxmain@zettascript.org>"]
|
"common",
|
||||||
license = "AGPL-3.0-only"
|
"server",
|
||||||
repository = "https://git.txmn.tk/tuxmain/webcomment"
|
"webui",
|
||||||
description = "Templatable comment web server"
|
]
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
argon2 = "0.4.1"
|
|
||||||
base64 = "0.21.0"
|
|
||||||
clap = { version = "4.0.32", default-features = false, features = ["derive", "error-context", "help", "std", "usage"] }
|
|
||||||
crossbeam-channel = "0.5.6"
|
|
||||||
directories = "4.0.1"
|
|
||||||
fluent-bundle = "0.15.2"
|
|
||||||
fluent-langneg = "0.13.0"
|
|
||||||
intl-memoizer = "0.5.1"
|
|
||||||
log = "0.4.17"
|
|
||||||
matrix-sdk = { version = "0.6.2", default-features = false, features = ["rustls-tls"] }
|
|
||||||
percent-encoding = "2.2.0"
|
|
||||||
petname = { version = "1.1.3", optional = true, default-features = false, features = ["std_rng", "default_dictionary"] }
|
|
||||||
rand = "0.8.5"
|
|
||||||
rand_core = { version = "0.6.4", features = ["std"] }
|
|
||||||
rpassword = "7.2.0"
|
|
||||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
|
||||||
serde_json = "1.0.91"
|
|
||||||
sha2 = "0.10.6"
|
|
||||||
sled = "0.34.7"
|
|
||||||
tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
|
|
||||||
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
|
|
||||||
tokio = { version = "1.24.1", features = ["macros", "rt-multi-thread"] }
|
|
||||||
toml_edit = { version = "0.17.1", features = ["easy"] }
|
|
||||||
typed-sled = "0.2.3"
|
|
||||||
unic-langid = { version = "0.9.1", features = ["macros"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["petname"]
|
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -9,12 +9,28 @@ Rust webserver for comments, that you can easily embed in a website.
|
||||||
* List and post comments by topic (e.g. each article in your blog is a topic)
|
* List and post comments by topic (e.g. each article in your blog is a topic)
|
||||||
* Admin approval
|
* Admin approval
|
||||||
* Admin notification on new comment via Matrix
|
* Admin notification on new comment via Matrix
|
||||||
* Embedded one-file webserver
|
* Single-file webserver, WASM client for browsers
|
||||||
* Customizable [Tera](https://github.com/Keats/tera) templates
|
* Customizable [Tera](https://github.com/Keats/tera) templates
|
||||||
* Comment frequency limit per IP
|
* Comment frequency limit per IP
|
||||||
* i18n
|
* i18n
|
||||||
* Petnames! (anonymous comment authors get a funny random name)
|
* Petnames! (anonymous comment authors get a funny random name)
|
||||||
* Designed for privacy and moderation
|
* Designed for privacy and moderation
|
||||||
|
* JSON API
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install trunk
|
||||||
|
cargo install trunk
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
cargo run --release -- start
|
||||||
|
|
||||||
|
# Build and serve client
|
||||||
|
cd webui
|
||||||
|
trunk build --release
|
||||||
|
python -m http.server -d dist
|
||||||
|
```
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
|
|
@ -47,16 +63,6 @@ Uses no cookie, no unique user identifier. At each mutation (i.e. new comment or
|
||||||
|
|
||||||
However, keep in mind that if a reverse proxy (or any other intermediate tool) is used, IP addresses and other metadata may be logged somewhere.
|
However, keep in mind that if a reverse proxy (or any other intermediate tool) is used, IP addresses and other metadata may be logged somewhere.
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
/api/post_comment
|
|
||||||
/api/comments_by_topic
|
|
||||||
/api/edit_comment
|
|
||||||
/api/remove_comment
|
|
||||||
/api/get_comment
|
|
||||||
/api/admin/approve_comment
|
|
||||||
/api/admin/remove_comment
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
CopyLeft 2022-2023 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
CopyLeft 2022-2023 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
||||||
|
|
|
||||||
15
client-js/basic.html
Normal file
15
client-js/basic.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>Webcomment</title>
|
||||||
|
<script type="text/javascript" src="js/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="js/webcomment.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="comments"></div>
|
||||||
|
<input type="button" value="Admin" onclick="webcomments['comments'].prompt_admin_psw()"/>
|
||||||
|
<script type="text/javascript">webcomment_topic("comments", "http://127.0.0.1:31720", "test");</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
client-js/js/jquery.js
vendored
Normal file
2
client-js/js/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
278
client-js/js/webcomment.js
Normal file
278
client-js/js/webcomment.js
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
CopyLeft 2022-2023 Pascal Engélibert (why copyleft? -> https://txmn.tk/blog/why-copyleft/)
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||||
|
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var webcomments = {};
|
||||||
|
|
||||||
|
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_pending_comment: `
|
||||||
|
<div class="comment comment-{{comment.id}}" id="{{root.id}}-pending-{{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>
|
||||||
|
<span class="comment-email"></span>
|
||||||
|
<span class="comment-addr"></span>
|
||||||
|
<a class="comment-edition comment-edition-remove" href="#">Remove</a>
|
||||||
|
</p>
|
||||||
|
<p class="comment-text"></p>
|
||||||
|
</div>`,
|
||||||
|
template_approved_comment: `
|
||||||
|
<div class="comment comment-{{comment.id}}" 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>
|
||||||
|
<span class="comment-email"></span>
|
||||||
|
<a class="comment-edition comment-edition-remove" href="#">Remove</a>
|
||||||
|
</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 {
|
||||||
|
constructor(root_id, api, mode, mode_param, config) {
|
||||||
|
this.root_id = root_id;
|
||||||
|
this.api = api;
|
||||||
|
this.mode = mode;
|
||||||
|
this.mode_param = mode_param;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
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];
|
||||||
|
this.comments = [];
|
||||||
|
|
||||||
|
switch(mode) {
|
||||||
|
case MODE_TOPIC:
|
||||||
|
var this_ = this;
|
||||||
|
this.query_comments_by_topic(mode_param.topic, function(resp) {
|
||||||
|
this_.append_comments(resp.approved_comments);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Webcomment: invalid mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query_comments_by_topic(topic, success) {
|
||||||
|
if("admin_psw" in this) {
|
||||||
|
$.ajax({
|
||||||
|
method: "POST",
|
||||||
|
url: this.api+"/api/admin/comments_by_topic",
|
||||||
|
data: JSON.stringify({
|
||||||
|
admin_psw: this.admin_psw,
|
||||||
|
topic: topic,
|
||||||
|
}),
|
||||||
|
success: success,
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
method: "POST",
|
||||||
|
url: this.api+"/api/comments_by_topic",
|
||||||
|
data: JSON.stringify({
|
||||||
|
mutation_token: "",
|
||||||
|
topic: topic,
|
||||||
|
}),
|
||||||
|
success: success,
|
||||||
|
dataType: "json",
|
||||||
|
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(approved_comments, pending_comments=[]) {
|
||||||
|
var this_ = this;
|
||||||
|
for(var i in pending_comments) {
|
||||||
|
var comment = pending_comments[i];
|
||||||
|
this.comments[comment.id] = comment;
|
||||||
|
|
||||||
|
var post_time = new Date(comment.post_time*1000);
|
||||||
|
|
||||||
|
var comment_html = this.config.template_pending_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+"-pending-"+comment.id);
|
||||||
|
elem.getElementsByClassName("comment-author")[0].innerHTML = comment.author;
|
||||||
|
elem.getElementsByClassName("comment-text")[0].innerHTML = comment.text;
|
||||||
|
if("email" in comment)
|
||||||
|
elem.getElementsByClassName("comment-email")[0].innerHTML = comment.email;
|
||||||
|
else
|
||||||
|
elem.getElementsByClassName("comment-email")[0].remove();
|
||||||
|
if("addr" in comment)
|
||||||
|
elem.getElementsByClassName("comment-addr")[0].innerHTML = comment.addr;
|
||||||
|
else
|
||||||
|
elem.getElementsByClassName("comment-addr")[0].remove();
|
||||||
|
if(comment.editable) {
|
||||||
|
var edition_remove_elems = elem.getElementsByClassName("comment-edition-remove");
|
||||||
|
console.log(edition_remove_elems);
|
||||||
|
for(var j = 0; j < edition_remove_elems.length; j ++) {
|
||||||
|
edition_remove_elems[j].onclick = function() {
|
||||||
|
this_.remove_comment(comment.id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var edition_elems = elem.getElementsByClassName("comment-edition");
|
||||||
|
for(var j = 0; j < edition_elems.length; j ++) {
|
||||||
|
edition_elems[j].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(var i in approved_comments) {
|
||||||
|
var comment = approved_comments[i];
|
||||||
|
this.comments[comment.id] = comment;
|
||||||
|
|
||||||
|
var post_time = new Date(comment.post_time*1000);
|
||||||
|
|
||||||
|
var comment_html = this.config.template_approved_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;
|
||||||
|
if("email" in comment)
|
||||||
|
elem.getElementsByClassName("comment-email")[0].innerHTML = comment.email;
|
||||||
|
else
|
||||||
|
elem.getElementsByClassName("comment-email")[0].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_comment(comment_id) {
|
||||||
|
var this_ = this;
|
||||||
|
if(this.admin_psw) {
|
||||||
|
$.ajax({
|
||||||
|
method: "POST",
|
||||||
|
url: this.api+"/api/admin/remove_comment",
|
||||||
|
data: JSON.stringify({
|
||||||
|
admin_psw: this.admin_psw,
|
||||||
|
comment_id: comment_id,
|
||||||
|
}),
|
||||||
|
success: function(resp) {
|
||||||
|
console.log(resp);
|
||||||
|
// TODO check resp
|
||||||
|
var comment_elems = this_.elem_comments.getElementsByClassName("comment-"+comment_id);
|
||||||
|
for(var j = 0; j < comment_elems.length; j ++) {
|
||||||
|
comment_elems[j].remove();
|
||||||
|
}
|
||||||
|
var comment_elems = this_.elem_comments.getElementsByClassName("comment-pending-"+comment_id);
|
||||||
|
for(var j = 0; j < comment_elems.length; j ++) {
|
||||||
|
comment_elems[j].remove();
|
||||||
|
}
|
||||||
|
this_.comments[comment_id]
|
||||||
|
},
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_admin_psw() {
|
||||||
|
this.admin_psw = prompt("Admin password");
|
||||||
|
if(this.admin_psw == null)
|
||||||
|
return;
|
||||||
|
switch(this.mode) {
|
||||||
|
case MODE_TOPIC:
|
||||||
|
var this_ = this;
|
||||||
|
this.query_comments_by_topic(this.mode_param.topic, function(resp) {
|
||||||
|
this_.elem_comments.innerHTML = "";
|
||||||
|
this_.append_comments(resp.approved_comments, resp.pending_comments);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Webcomment: invalid mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function webcomment_topic(root_id, api, topic, config=DEFAULT_CONFIG) {
|
||||||
|
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-js/style.css
Normal file
9
client-js/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
CopyLeft 2022-2023 Pascal Engélibert (why copyleft? -> https://txmn.tk/blog/why-copyleft/)
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
|
||||||
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var webcomments = {};
|
|
||||||
|
|
||||||
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 {
|
|
||||||
constructor(root_id, api, mode, mode_param, config) {
|
|
||||||
this.root_id = root_id;
|
|
||||||
this.api = api;
|
|
||||||
this.mode = mode;
|
|
||||||
this.mode_param = mode_param;
|
|
||||||
this.config = config;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
case MODE_TOPIC:
|
|
||||||
var this_ = this;
|
|
||||||
this.query_comments_by_topic(mode_param.topic, function(resp) {
|
|
||||||
this_.append_comments(resp.comments);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Webcomment: invalid mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query_comments_by_topic(topic, success) {
|
|
||||||
$.ajax({
|
|
||||||
method: "POST",
|
|
||||||
url: this.api+"/api/comments_by_topic",
|
|
||||||
data: JSON.stringify({
|
|
||||||
mutation_token: "",
|
|
||||||
topic: topic,
|
|
||||||
}),
|
|
||||||
success: success,
|
|
||||||
dataType: "json",
|
|
||||||
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, config=DEFAULT_CONFIG) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
20
common/Cargo.toml
Normal file
20
common/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "webcomment-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["tuxmain <tuxmain@zettascript.org>"]
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
repository = "https://git.txmn.tk/tuxmain/webcomment"
|
||||||
|
description = "Templatable comment web server"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argon2 = "0.5.0"
|
||||||
|
base64 = "0.21.2"
|
||||||
|
log = "0.4.19"
|
||||||
|
percent-encoding = "2.3.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
|
serde = { version = "1.0.171", features = ["derive", "rc"] }
|
||||||
|
serde_json = "1.0.100"
|
||||||
|
sha2 = "0.10.7"
|
||||||
|
unic-langid = { version = "0.9.1", features = ["macros"] }
|
||||||
2
common/src/api.rs
Normal file
2
common/src/api.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod queries;
|
||||||
|
pub mod resps;
|
||||||
34
common/src/api/queries.rs
Normal file
34
common/src/api/queries.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/*#[derive(Deserialize)]
|
||||||
|
pub struct Admin<T> {
|
||||||
|
pub admin_psw: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub query: T,
|
||||||
|
}*/
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct CommentsByTopic {
|
||||||
|
pub mutation_token: Option<String>,
|
||||||
|
pub topic: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct CommentsByTopicAdmin {
|
||||||
|
pub admin_psw: String,
|
||||||
|
pub topic: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct NewComment {
|
||||||
|
pub author: String,
|
||||||
|
pub email: String,
|
||||||
|
pub text: String,
|
||||||
|
pub topic: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct RemoveCommentAdmin {
|
||||||
|
pub admin_psw: String,
|
||||||
|
pub comment_id: String,
|
||||||
|
}
|
||||||
90
common/src/api/resps.rs
Normal file
90
common/src/api/resps.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/*/// Use Ok only when there is no dedicated struct
|
||||||
|
/// (because serde doesn't allow flattening enums)
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum Result {
|
||||||
|
Ok,
|
||||||
|
#[serde(rename = "error")]
|
||||||
|
Err(Error),
|
||||||
|
}*/
|
||||||
|
|
||||||
|
pub type Result = std::result::Result<Response, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub enum Error {
|
||||||
|
Antispam {
|
||||||
|
timeout: Time,
|
||||||
|
},
|
||||||
|
BadAdminAuth,
|
||||||
|
IllegalContent,
|
||||||
|
Internal,
|
||||||
|
InvalidRequest,
|
||||||
|
/// Admin only! Error messages may contain sensitive information.
|
||||||
|
Message(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub enum Response {
|
||||||
|
CommentsByTopic(CommentsByTopic),
|
||||||
|
CommentsByTopicAdmin(CommentsByTopicAdmin),
|
||||||
|
NewComment(NewComment),
|
||||||
|
Ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#[derive(Serialize)]
|
||||||
|
pub struct GenericOk {}*/
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct CommentsByTopic {
|
||||||
|
pub approved_comments: Vec<ApprovedCommentWithMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct CommentsByTopicAdmin {
|
||||||
|
pub approved_comments: Vec<ApprovedCommentWithMeta>,
|
||||||
|
pub pending_comments: Vec<PendingCommentWithMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct OriginalComment {
|
||||||
|
pub author: String,
|
||||||
|
pub editable: bool,
|
||||||
|
pub last_edit_time: Option<Time>,
|
||||||
|
pub post_time: Time,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ApprovedCommentWithMeta {
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub author: String,
|
||||||
|
pub editable: bool,
|
||||||
|
pub id: String,
|
||||||
|
pub last_edit_time: Option<Time>,
|
||||||
|
pub post_time: Time,
|
||||||
|
pub status: CommentStatus,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct PendingCommentWithMeta {
|
||||||
|
pub addr: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub author: String,
|
||||||
|
pub editable: bool,
|
||||||
|
pub id: String,
|
||||||
|
pub last_edit_time: Option<Time>,
|
||||||
|
pub post_time: Time,
|
||||||
|
pub status: CommentStatus,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct NewComment {
|
||||||
|
pub id: String,
|
||||||
|
pub mutation_token: String,
|
||||||
|
pub post_time: Time,
|
||||||
|
}
|
||||||
2
common/src/lib.rs
Normal file
2
common/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod api;
|
||||||
|
pub mod types;
|
||||||
129
common/src/types.rs
Normal file
129
common/src/types.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
use base64::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
pub type Time = u64;
|
||||||
|
|
||||||
|
pub const BASE64: base64::engine::general_purpose::GeneralPurpose =
|
||||||
|
base64::engine::general_purpose::GeneralPurpose::new(
|
||||||
|
&base64::alphabet::URL_SAFE,
|
||||||
|
base64::engine::general_purpose::NO_PAD,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct MutationToken(pub [u8; 16]);
|
||||||
|
|
||||||
|
impl Default for MutationToken {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self([0; 16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MutationToken {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(rand::random())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
|
||||||
|
std::panic::catch_unwind(|| {
|
||||||
|
let mut buf = [0; 16];
|
||||||
|
BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
|
||||||
|
Ok(Self(buf))
|
||||||
|
})
|
||||||
|
.map_err(|_| base64::DecodeError::InvalidLength)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for MutationToken {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct TopicHash(pub [u8; 32]);
|
||||||
|
|
||||||
|
impl Default for TopicHash {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self([0; 32])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TopicHash {
|
||||||
|
pub fn from_topic(topic: &str) -> Self {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(topic.as_bytes());
|
||||||
|
Self(hasher.finalize().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for TopicHash {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct CommentId(pub [u8; 16]);
|
||||||
|
|
||||||
|
impl Default for CommentId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self([0; 16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommentId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(rand::random())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self([0; 16])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max() -> Self {
|
||||||
|
Self([255; 16])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_base64(&self) -> String {
|
||||||
|
BASE64.encode(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
|
||||||
|
std::panic::catch_unwind(|| {
|
||||||
|
let mut buf = [0; 16];
|
||||||
|
BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
|
||||||
|
Ok(Self(buf))
|
||||||
|
})
|
||||||
|
.map_err(|_| base64::DecodeError::InvalidLength)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for CommentId {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum CommentStatus {
|
||||||
|
Pending = 0,
|
||||||
|
Approved = 1,
|
||||||
|
ApprovedEdited(Comment) = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct Comment {
|
||||||
|
pub author: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub last_edit_time: Option<u64>,
|
||||||
|
pub mutation_token: MutationToken,
|
||||||
|
pub post_time: u64,
|
||||||
|
pub text: String,
|
||||||
|
pub topic_hash: TopicHash,
|
||||||
|
}
|
||||||
3523
server/Cargo.lock
generated
Normal file
3523
server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
41
server/Cargo.toml
Normal file
41
server/Cargo.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
[package]
|
||||||
|
name = "webcomment-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["tuxmain <tuxmain@zettascript.org>"]
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
repository = "https://git.txmn.tk/tuxmain/webcomment"
|
||||||
|
description = "Templatable comment web server"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "webcomment-server"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
webcomment-common = { path = "../common" }
|
||||||
|
|
||||||
|
argon2 = "0.5.0"
|
||||||
|
base64 = "0.21.2"
|
||||||
|
clap = { version = "4.3.11", default-features = false, features = ["derive", "error-context", "help", "std", "usage"] }
|
||||||
|
crossbeam-channel = "0.5.8"
|
||||||
|
directories = "5.0.1"
|
||||||
|
fluent-bundle = "0.15.2"
|
||||||
|
fluent-langneg = "0.13.0"
|
||||||
|
intl-memoizer = "0.5.1"
|
||||||
|
log = "0.4.19"
|
||||||
|
matrix-sdk = { version = "0.6.2", default-features = false, features = ["rustls-tls"] }
|
||||||
|
percent-encoding = "2.3.0"
|
||||||
|
petname = { version = "1.1.3", optional = true, default-features = false, features = ["std_rng", "default_dictionary"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
|
rpassword = "7.2.0"
|
||||||
|
serde = { version = "1.0.171", features = ["derive", "rc"] }
|
||||||
|
serde_json = "1.0.100"
|
||||||
|
sha2 = "0.10.7"
|
||||||
|
sled = "0.34.7"
|
||||||
|
tera = { version = "1.19.0", features = ["builtins", "date-locale"] }
|
||||||
|
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
|
||||||
|
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
toml = "0.7.6"
|
||||||
|
typed-sled = "0.2.3"
|
||||||
|
unic-langid = { version = "0.9.1", features = ["macros"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["petname"]
|
||||||
28
server/locales/en.ftl
Normal file
28
server/locales/en.ftl
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
admin-comment-approve = Approve
|
||||||
|
admin-comment-approve-edit = Approve
|
||||||
|
admin-comment-edit = Edit
|
||||||
|
admin-comment-edit-edit = Edit
|
||||||
|
admin-comment-remove = Remove
|
||||||
|
admin-comment-remove-edit = Remove
|
||||||
|
admin_login-password_prompt = Password:
|
||||||
|
admin_login-submit_button = Login
|
||||||
|
admin_login-title = Admin login | Comments
|
||||||
|
edit_comment-success = Your edit has been published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||||
|
edit_comment-success_pending = Your edit has been saved. It will be reviewed by an administrator before being published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||||
|
error-antispam = The edition quota from your IP is reached. You will be unblocked in { $antispam_timeout }s.
|
||||||
|
error-comment-author_name_too_long = Author name length is { $len } but maximum is { $max_len }.
|
||||||
|
error-comment-email_too_long = E-mail length is { $len } but maximum is { $max_len }.
|
||||||
|
error-comment-text_too_long = Comment length is { $len } but maximum is { $max_len }.
|
||||||
|
error-list =
|
||||||
|
{ $nb_errors ->
|
||||||
|
[one] Whoops, the following error occurred:
|
||||||
|
*[other] Whoops, the following { $nb_errors } errors occurred:
|
||||||
|
}
|
||||||
|
comment_form-author = Your name:
|
||||||
|
comment_form-email = Your email:
|
||||||
|
comment_form-edit_button = Edit comment
|
||||||
|
comment_form-new_button = Post comment
|
||||||
|
comment_form-text = Your comment:
|
||||||
|
new_comment-success = Your comment has been published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||||
|
new_comment-success_pending = Your comment has been saved. It will be reviewed by an administrator before being published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||||
|
title = Comments
|
||||||
18
server/locales/fr.ftl
Normal file
18
server/locales/fr.ftl
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
admin-comment-approve = Approuver
|
||||||
|
admin-comment-edit = Modifier
|
||||||
|
admin-comment-remove = Supprimer
|
||||||
|
admin_login-password_prompt = Mot de passe :
|
||||||
|
admin_login-submit_button = S'authentifier
|
||||||
|
admin_login-title = Authentification admin | Commentaires
|
||||||
|
error-antispam = Le quota d'édition de votre adresse IP est atteint, elle sera débloquée dans { $antispam_timeout }s.
|
||||||
|
error-list =
|
||||||
|
{ $nb_errors ->
|
||||||
|
[one] Oups, l'erreur suivante est survenue :
|
||||||
|
*[other] Oups, les { $nb_errors } erreurs suivantes sont survenues :
|
||||||
|
}
|
||||||
|
comment_form-author = Votre nom :
|
||||||
|
comment_form-email = Votre e-mail :
|
||||||
|
comment_form-edit_button = Modifier
|
||||||
|
comment_form-new_button = Envoyer
|
||||||
|
comment_form-text = Votre commentaire :
|
||||||
|
title = Commentaires
|
||||||
30
server/src/cleaner.rs
Normal file
30
server/src/cleaner.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::{config::Config, db::*};
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub async fn run_cleaner(config: &Config, dbs: Dbs) {
|
||||||
|
let mut last_db_clean = 0;
|
||||||
|
loop {
|
||||||
|
let time = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if time > last_db_clean + 3600 {
|
||||||
|
clean_antispam(config, dbs.clone(), time);
|
||||||
|
last_db_clean = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_antispam(config: &Config, dbs: Dbs, time: u64) {
|
||||||
|
for (addr, (last_mutation, _mutation_count)) in
|
||||||
|
dbs.client_mutation.iter().filter_map(|o| o.ok())
|
||||||
|
{
|
||||||
|
if last_mutation + config.antispam_duration < time {
|
||||||
|
dbs.client_mutation.remove(&addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue