Matrix notification
This commit is contained in:
parent
86495543ce
commit
2de26f5ffc
7 changed files with 1090 additions and 17 deletions
949
Cargo.lock
generated
949
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,20 +9,20 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.4.1"
|
argon2 = "0.4.1"
|
||||||
async-std = { version = "1.12.0", features = ["attributes"] }
|
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
clap = { version = "4.0.15", default-features = false, features = ["derive", "std"] }
|
clap = { version = "4.0.15", default-features = false, features = ["derive", "std"] }
|
||||||
|
crossbeam-channel = "0.5.6"
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
#matrix-sdk = { version = "0.5.0", default-features = false }
|
matrix-sdk = { version = "0.6.0", default-features = false, features = ["rustls-tls"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rand_core = { version = "0.6.4", features = ["std"] }
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
rpassword = "7.0.0"
|
rpassword = "7.0.0"
|
||||||
serde = { version = "1.0.145", features = ["derive", "rc"] }
|
serde = { version = "1.0.145", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.86"
|
|
||||||
sha2 = "0.10.6"
|
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.21.2", features = ["macros", "rt-multi-thread"] }
|
||||||
toml_edit = { version = "0.14.4", features = ["easy"] }
|
toml_edit = { version = "0.14.4", features = ["easy"] }
|
||||||
typed-sled = "0.2.0"
|
typed-sled = "0.2.0"
|
||||||
|
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# webcomment
|
||||||
|
|
||||||
|
Rust webserver for comments, that you can easily embed in a website.
|
||||||
|
|
||||||
|
**Early development, not safe for production yet**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* List and post comments by topic (e.g. each article in your blog is a topic)
|
||||||
|
* Admin approval
|
||||||
|
* Admin notification on new comment via Matrix
|
||||||
|
* Embedded one-file webserver
|
||||||
|
* [Tera](https://github.com/Keats/tera) templates
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
webcomment init
|
||||||
|
|
||||||
|
# This adds an admin password to the config (password are hashed)
|
||||||
|
webcomment psw
|
||||||
|
|
||||||
|
# edit ~/.config/webcomment/config.toml
|
||||||
|
|
||||||
|
webcomment start
|
||||||
|
|
||||||
|
Each topic is accessible at `/t/<topic_name>`.
|
||||||
|
|
||||||
|
Admin login is accessible at `/admin`. Once authenticated, you can see the pending comments on the topic pages.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
CopyLeft 2022 Pascal Engélibert [(why copyleft?)](//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/.
|
|
@ -32,6 +32,17 @@ pub struct Config {
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
#[serde(default = "Config::default_listen")]
|
#[serde(default = "Config::default_listen")]
|
||||||
pub listen: SocketAddr,
|
pub listen: SocketAddr,
|
||||||
|
/// Send a matrix message on new comment
|
||||||
|
#[serde(default = "Config::default_matrix_notify")]
|
||||||
|
pub matrix_notify: bool,
|
||||||
|
#[serde(default = "Config::default_matrix_password")]
|
||||||
|
pub matrix_password: String,
|
||||||
|
#[serde(default = "Config::default_matrix_room")]
|
||||||
|
pub matrix_room: String,
|
||||||
|
#[serde(default = "Config::default_matrix_server")]
|
||||||
|
pub matrix_server: String,
|
||||||
|
#[serde(default = "Config::default_matrix_user")]
|
||||||
|
pub matrix_user: String,
|
||||||
#[serde(default = "Config::default_root_url")]
|
#[serde(default = "Config::default_root_url")]
|
||||||
pub root_url: String,
|
pub root_url: String,
|
||||||
}
|
}
|
||||||
|
@ -70,6 +81,21 @@ impl Config {
|
||||||
fn default_listen() -> SocketAddr {
|
fn default_listen() -> SocketAddr {
|
||||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 31720)
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 31720)
|
||||||
}
|
}
|
||||||
|
fn default_matrix_notify() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn default_matrix_password() -> String {
|
||||||
|
"".into()
|
||||||
|
}
|
||||||
|
fn default_matrix_room() -> String {
|
||||||
|
"#maintenance:matrix.txmn.tk".into()
|
||||||
|
}
|
||||||
|
fn default_matrix_server() -> String {
|
||||||
|
"https://matrix.txmn.tk".into()
|
||||||
|
}
|
||||||
|
fn default_matrix_user() -> String {
|
||||||
|
"@tuxmain:matrix.txmn.tk".into()
|
||||||
|
}
|
||||||
fn default_root_url() -> String {
|
fn default_root_url() -> String {
|
||||||
"/".into()
|
"/".into()
|
||||||
}
|
}
|
||||||
|
@ -89,6 +115,11 @@ impl Default for Config {
|
||||||
cookies_domain: Self::default_cookies_domain(),
|
cookies_domain: Self::default_cookies_domain(),
|
||||||
lang: Self::default_lang(),
|
lang: Self::default_lang(),
|
||||||
listen: Self::default_listen(),
|
listen: Self::default_listen(),
|
||||||
|
matrix_notify: Self::default_matrix_notify(),
|
||||||
|
matrix_password: Self::default_matrix_password(),
|
||||||
|
matrix_room: Self::default_matrix_room(),
|
||||||
|
matrix_server: Self::default_matrix_server(),
|
||||||
|
matrix_user: Self::default_matrix_user(),
|
||||||
root_url: Self::default_root_url(),
|
root_url: Self::default_root_url(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +138,7 @@ pub fn read_config(dir: &Path) -> Config {
|
||||||
std::str::from_utf8(&std::fs::read(path).expect("Cannot read config file"))
|
std::str::from_utf8(&std::fs::read(path).expect("Cannot read config file"))
|
||||||
.expect("Bad encoding in config file"),
|
.expect("Bad encoding in config file"),
|
||||||
)
|
)
|
||||||
.expect("Bad JSON in config file")
|
.expect("Bad TOML in config file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
mod notify;
|
||||||
mod queries;
|
mod queries;
|
||||||
mod server;
|
mod server;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
@ -12,7 +13,7 @@ use argon2::{
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
#[async_std::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let opt = cli::MainOpt::parse();
|
let opt = cli::MainOpt::parse();
|
||||||
|
|
||||||
|
|
64
src/notify.rs
Normal file
64
src/notify.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use matrix_sdk::ruma;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
struct Notifier {
|
||||||
|
matrix: Option<(matrix_sdk::Client, matrix_sdk::room::Joined)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notifier {
|
||||||
|
async fn new(config: &Config) -> Self {
|
||||||
|
Self {
|
||||||
|
matrix: if config.matrix_notify {
|
||||||
|
let user = ruma::UserId::parse(&config.matrix_user).unwrap();
|
||||||
|
let client = matrix_sdk::Client::builder()
|
||||||
|
.homeserver_url(&config.matrix_server)
|
||||||
|
.user_agent("Webcomment")
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client
|
||||||
|
.login_username(&user, &config.matrix_password)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
client
|
||||||
|
.sync_once(matrix_sdk::config::SyncSettings::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let room_id = <&ruma::RoomId>::try_from(config.matrix_room.as_str()).unwrap();
|
||||||
|
let room = client.get_room(room_id).unwrap();
|
||||||
|
|
||||||
|
if let matrix_sdk::room::Room::Joined(room) = room {
|
||||||
|
Some((client, room))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn notify(&self) {
|
||||||
|
if let Some((_client, room)) = &self.matrix {
|
||||||
|
room.send(
|
||||||
|
ruma::events::room::message::RoomMessageEventContent::text_plain("New comment."),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_notifier(config: Arc<Config>, recv: Receiver<()>) {
|
||||||
|
let notifier = Notifier::new(&config).await;
|
||||||
|
for () in recv {
|
||||||
|
notifier.notify().await;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{config::*, db::*, helpers, queries::*, templates::*};
|
use crate::{config::*, db::*, helpers, queries::*, templates::*};
|
||||||
|
|
||||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
@ -11,6 +12,9 @@ pub async fn start_server(config: Config, dbs: Dbs, templates: Templates) {
|
||||||
let templates = Arc::new(templates);
|
let templates = Arc::new(templates);
|
||||||
let config = Arc::new(config);
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let (notify_send, notify_recv) = crossbeam_channel::bounded(10);
|
||||||
|
tokio::spawn(crate::notify::run_notifier(config.clone(), notify_recv));
|
||||||
|
|
||||||
let mut app = tide::new();
|
let mut app = tide::new();
|
||||||
app.at(&format!("{}t/:topic", config.root_url)).get({
|
app.at(&format!("{}t/:topic", config.root_url)).get({
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
@ -25,7 +29,13 @@ pub async fn start_server(config: Config, dbs: Dbs, templates: Templates) {
|
||||||
let templates = templates.clone();
|
let templates = templates.clone();
|
||||||
let dbs = dbs.clone();
|
let dbs = dbs.clone();
|
||||||
move |req: tide::Request<()>| {
|
move |req: tide::Request<()>| {
|
||||||
handle_post_comments(req, config.clone(), templates.clone(), dbs.clone())
|
handle_post_comments(
|
||||||
|
req,
|
||||||
|
config.clone(),
|
||||||
|
templates.clone(),
|
||||||
|
dbs.clone(),
|
||||||
|
notify_send.clone(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.at(&format!("{}admin", config.root_url)).get({
|
app.at(&format!("{}admin", config.root_url)).get({
|
||||||
|
@ -176,6 +186,7 @@ async fn handle_post_comments(
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
templates: Arc<Templates>,
|
templates: Arc<Templates>,
|
||||||
dbs: Dbs,
|
dbs: Dbs,
|
||||||
|
notify_send: Sender<()>,
|
||||||
) -> tide::Result<tide::Response> {
|
) -> tide::Result<tide::Response> {
|
||||||
match req.body_form::<CommentQuery>().await? {
|
match req.body_form::<CommentQuery>().await? {
|
||||||
CommentQuery::NewComment(query) => {
|
CommentQuery::NewComment(query) => {
|
||||||
|
@ -215,6 +226,7 @@ async fn handle_post_comments(
|
||||||
helpers::new_pending_comment(&comment, &dbs)
|
helpers::new_pending_comment(&comment, &dbs)
|
||||||
.map_err(|e| error!("Adding pending comment: {:?}", e))
|
.map_err(|e| error!("Adding pending comment: {:?}", e))
|
||||||
.ok();
|
.ok();
|
||||||
|
notify_send.send(()).ok();
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue