diff --git a/Cargo.lock b/Cargo.lock index 2bf57d4..6aef1a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,12 +38,78 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -73,9 +139,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" @@ -85,9 +151,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.25" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "shlex", ] @@ -114,6 +180,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -336,6 +403,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -348,6 +421,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -599,6 +673,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" @@ -636,6 +716,7 @@ name = "noisebell" version = "0.1.0" dependencies = [ "anyhow", + "axum", "chrono", "futures", "reqwest", @@ -1037,6 +1118,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1299,6 +1390,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1337,6 +1429,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 31df3b1..12f86ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,13 +5,14 @@ edition = "2021" [dependencies] anyhow = "1.0" -tokio = { version = "1.0", features = ["full"] } +tokio = { version = "1.36", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" rppal = "0.22" reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features=false} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -chrono = "0.4" -futures = "0.3.31" -tracing-appender = "0.2.3" +chrono = { version = "0.4", features = ["serde"] } +futures = "0.3" +tracing-appender = "0.2" +axum = "0.7" diff --git a/deploy.sh b/deploy.sh index a3bab98..5fdd495 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,10 +7,10 @@ echo "Building for Raspberry Pi..." cross build --release --target aarch64-unknown-linux-gnu echo "Copying to Raspberry Pi..." -scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/ -scp endpoints.json noisebridge@noisebell.local:/home/noisebridge/endpoints.json +scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/noisebell/ +scp endpoints.json noisebridge@noisebell.local:/home/noisebridge/noisebell/endpoints.json echo "Setting permissions" -ssh noisebridge@noisebell.local "chmod +x ~/noisebell " +ssh noisebridge@noisebell.local "chmod +x ~/noisebell/noisebell" echo "Deployment complete!" diff --git a/endpoints.json b/endpoints.json index acd5f1b..c1be358 100644 --- a/endpoints.json +++ b/endpoints.json @@ -1,16 +1,4 @@ { "endpoints": [ - { - "url": "http://localhost:8080/webhook", - "description": "Local development endpoint" - }, - { - "url": "https://api.example.com/notifications", - "description": "Production notification service" - }, - { - "url": "https://webhook.site/your-unique-id", - "description": "Webhook testing service" - } ] } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 94b8977..a74537f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,15 @@ mod webhook; use std::time::Duration; use std::fs; +use std::sync::Arc; use anyhow::Result; +use axum::{ + routing::post, + Router, + Json, + extract::State, +}; use tracing::{error, info}; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; @@ -12,8 +19,10 @@ use tracing_subscriber::filter::LevelFilter; #[tokio::main] async fn main() -> Result<()> { + info!("creating logs directory"); fs::create_dir_all("logs")?; + info!("initializing logging"); let file_appender = RollingFileAppender::builder() .rotation(Rotation::DAILY) .filename_prefix("noisebell") @@ -35,23 +44,26 @@ async fn main() -> Result<()> { .with(fmt::Layer::default().with_writer(non_blocking)) .init(); - info!("Starting noisebell..."); - const DEFAULT_GPIO_PIN: u8 = 17; const DEFAULT_WEBHOOK_RETRIES: u32 = 3; + const DEFAULT_SERVER_PORT: u16 = 8080; - let gpio_pin = std::env::var("GPIO_PIN") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_GPIO_PIN); + info!("initializing webhook notifier"); + let webhook_notifier = Arc::new(webhook::WebhookNotifier::new(DEFAULT_WEBHOOK_RETRIES)?); - let webhook_retries = std::env::var("WEBHOOK_RETRIES") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_WEBHOOK_RETRIES); + info!("initializing gpio monitor"); + let mut gpio_monitor = gpio::GpioMonitor::new(DEFAULT_GPIO_PIN, Duration::from_millis(100))?; - let webhook_notifier = webhook::WebhookNotifier::new(webhook_retries)?; - let mut gpio_monitor = gpio::GpioMonitor::new(gpio_pin, Duration::from_millis(100))?; + let app = Router::new() + .route("/endpoints", post(add_endpoint)) + .with_state(webhook_notifier.clone()); + + let server_addr = format!("127.0.0.1:{}", DEFAULT_SERVER_PORT); + info!("Starting API server on http://{}", server_addr); + + let listener = tokio::net::TcpListener::bind(&server_addr).await?; + axum::serve(listener, app.into_make_service()) + .await?; let callback = move |event: gpio::CircuitEvent| { info!("Circuit state changed: {:?}", event); @@ -63,7 +75,7 @@ async fn main() -> Result<()> { }); }; - info!("starting gpio_monitor"); + info!("starting GPIO monitor"); if let Err(e) = gpio_monitor.monitor(callback).await { error!("GPIO monitoring error: {}", e); @@ -71,3 +83,15 @@ async fn main() -> Result<()> { Ok(()) } + +async fn add_endpoint( + State(notifier): State>, + Json(endpoint): Json, +) -> Result<(), axum::http::StatusCode> { + notifier.add_endpoint(endpoint) + .await + .map_err(|e| { + error!("Failed to add endpoint: {}", e); + axum::http::StatusCode::INTERNAL_SERVER_ERROR + }) +} diff --git a/src/webhook.rs b/src/webhook.rs index 0dec54e..7398ce7 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -2,19 +2,21 @@ use anyhow::Result; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::fs; +use std::sync::Arc; +use tokio::sync::RwLock; use tracing::{error, info}; use std::time::Duration; use futures::future::join_all; use crate::gpio::CircuitEvent; -#[derive(Debug, Deserialize, Clone)] -struct Endpoint { +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Endpoint { url: String, description: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] struct EndpointsConfig { endpoints: Vec, } @@ -29,22 +31,47 @@ struct WebhookPayload { #[derive(Clone)] pub struct WebhookNotifier { client: Client, - endpoints: Vec, + endpoints: Arc>>, max_retries: u32, } impl WebhookNotifier { pub fn new(max_retries: u32) -> Result { - let config = fs::read_to_string("endpoints.json")?; - let endpoints_config: EndpointsConfig = serde_json::from_str(&config)?; + let endpoints = if let Ok(config) = fs::read_to_string("endpoints.json") { + let endpoints_config: EndpointsConfig = serde_json::from_str(&config)?; + endpoints_config.endpoints + } else { + Vec::new() + }; Ok(Self { client: Client::new(), - endpoints: endpoints_config.endpoints, + endpoints: Arc::new(RwLock::new(endpoints)), max_retries, }) } + pub async fn add_endpoint(&self, endpoint: Endpoint) -> Result<()> { + let mut endpoints = self.endpoints.write().await; + + endpoints.retain(|e| e.description != endpoint.description); + + endpoints.push(endpoint); + + self.save_endpoints().await?; + + Ok(()) + } + + async fn save_endpoints(&self) -> Result<()> { + let endpoints = self.endpoints.read().await; + let config = EndpointsConfig { + endpoints: endpoints.clone(), + }; + fs::write("endpoints.json", serde_json::to_string_pretty(&config)?)?; + Ok(()) + } + async fn send_webhook(&self, endpoint: &Endpoint, payload: &WebhookPayload) -> Result<()> { match self.client .post(&endpoint.url) @@ -106,7 +133,8 @@ impl WebhookNotifier { new_state: state.to_string(), }; - let webhook_futures: Vec<_> = self.endpoints.iter() + let endpoints = self.endpoints.read().await; + let webhook_futures: Vec<_> = endpoints.iter() .map(|endpoint| { info!("Sending webhook to {}: {}", endpoint.description, serde_json::to_string(&payload).unwrap()); self.send_webhook_with_retries(endpoint, &payload)