diff --git a/Cargo.lock b/Cargo.lock index feb5dc5c..c4f0c8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,7 @@ dependencies = [ "fs-err", "http", "rand", + "reqwest", "serde", "serde_json", "sodiumoxide", diff --git a/atuin-server/Cargo.toml b/atuin-server/Cargo.toml index 9a9c2890..59afa0ac 100644 --- a/atuin-server/Cargo.toml +++ b/atuin-server/Cargo.toml @@ -35,3 +35,7 @@ fs-err = "2.9" chronoutil = "0.2.3" tower = "0.4" tower-http = { version = "0.3", features = ["trace"] } +reqwest = { version = "0.11", features = [ + "json", + "rustls-tls-native-roots", +], default-features = false } diff --git a/atuin-server/src/handlers/user.rs b/atuin-server/src/handlers/user.rs index 9e145d3c..61af989c 100644 --- a/atuin-server/src/handlers/user.rs +++ b/atuin-server/src/handlers/user.rs @@ -1,4 +1,6 @@ use std::borrow::Borrow; +use std::collections::HashMap; +use std::time::Duration; use axum::{ extract::{Path, State}, @@ -6,7 +8,7 @@ use axum::{ }; use http::StatusCode; use sodiumoxide::crypto::pwhash::argon2id13; -use tracing::{debug, error, instrument}; +use tracing::{debug, error, info, instrument}; use uuid::Uuid; use super::{ErrorResponse, ErrorResponseStatus, RespExt}; @@ -16,6 +18,8 @@ use crate::{ router::AppState, }; +use reqwest::header::CONTENT_TYPE; + use atuin_common::api::*; pub fn verify_str(secret: &str, verify: &str) -> bool { @@ -32,6 +36,30 @@ pub fn verify_str(secret: &str, verify: &str) -> bool { } } +// Try to send a Discord webhook once - if it fails, we don't retry. "At most once", and best effort. +// Don't return the status because if this fails, we don't really care. +async fn send_register_hook(url: &str, username: String, registered: String) { + let hook = HashMap::from([ + ("username", username), + ("content", format!("{registered} has just signed up!")), + ]); + + let client = reqwest::Client::new(); + + let resp = client + .post(url) + .timeout(Duration::new(5, 0)) + .header(CONTENT_TYPE, "application/json") + .json(&hook) + .send() + .await; + + match resp { + Ok(_) => info!("register webhook sent ok!"), + Err(e) => error!("failed to send register webhook: {}", e), + } +} + #[instrument(skip_all, fields(user.username = username.as_str()))] pub async fn get( Path(username): Path, @@ -71,8 +99,8 @@ pub async fn register( let hashed = hash_secret(®ister.password); let new_user = NewUser { - email: register.email, - username: register.username, + email: register.email.clone(), + username: register.username.clone(), password: hashed, }; @@ -94,6 +122,16 @@ pub async fn register( token: (&token).into(), }; + if let Some(url) = &state.settings.register_webhook_url { + // Could probs be run on another thread, but it's ok atm + send_register_hook( + url, + state.settings.register_webhook_username.clone(), + register.username, + ) + .await; + } + match db.add_session(&new_session).await { Ok(_) => Ok(Json(RegisterResponse { session: token })), Err(e) => { diff --git a/atuin-server/src/settings.rs b/atuin-server/src/settings.rs index 8da0c0aa..b689983c 100644 --- a/atuin-server/src/settings.rs +++ b/atuin-server/src/settings.rs @@ -15,6 +15,8 @@ pub struct Settings { pub db_uri: String, pub open_registration: bool, pub max_history_length: usize, + pub register_webhook_url: Option, + pub register_webhook_username: String, } impl Settings { @@ -37,6 +39,7 @@ impl Settings { .set_default("open_registration", false)? .set_default("max_history_length", 8192)? .set_default("path", "")? + .set_default("register_webhook_username", "")? .add_source( Environment::with_prefix("atuin") .prefix_separator("_")