Matrix notification

This commit is contained in:
Pascal Engélibert 2022-10-15 23:48:06 +02:00
parent 86495543ce
commit 2de26f5ffc
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
7 changed files with 1090 additions and 17 deletions

949
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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
View 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/.

View file

@ -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")
} }
} }

View 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
View 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;
}
}

View file

@ -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!(),
} }