feat: allow requesting to register endpoints

This commit is contained in:
Jet Pham 2025-06-05 21:12:53 -07:00
parent 07dfe2d9bc
commit 5adf192e5d
No known key found for this signature in database
6 changed files with 178 additions and 44 deletions

101
Cargo.lock generated
View file

@ -38,12 +38,78 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.75"
@ -73,9 +139,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.17.0" version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]] [[package]]
name = "bytes" name = "bytes"
@ -85,9 +151,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.25" version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -114,6 +180,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link",
] ]
@ -336,6 +403,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.6.0" version = "1.6.0"
@ -348,6 +421,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@ -599,6 +673,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -636,6 +716,7 @@ name = "noisebell"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum",
"chrono", "chrono",
"futures", "futures",
"reqwest", "reqwest",
@ -1037,6 +1118,16 @@ dependencies = [
"serde", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1299,6 +1390,7 @@ dependencies = [
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@ -1337,6 +1429,7 @@ version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",

View file

@ -5,13 +5,14 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.36", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
rppal = "0.22" rppal = "0.22"
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features=false} reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features=false}
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
chrono = "0.4" chrono = { version = "0.4", features = ["serde"] }
futures = "0.3.31" futures = "0.3"
tracing-appender = "0.2.3" tracing-appender = "0.2"
axum = "0.7"

View file

@ -7,10 +7,10 @@ echo "Building for Raspberry Pi..."
cross build --release --target aarch64-unknown-linux-gnu cross build --release --target aarch64-unknown-linux-gnu
echo "Copying to Raspberry Pi..." echo "Copying to Raspberry Pi..."
scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/ scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/noisebell/
scp endpoints.json noisebridge@noisebell.local:/home/noisebridge/endpoints.json scp endpoints.json noisebridge@noisebell.local:/home/noisebridge/noisebell/endpoints.json
echo "Setting permissions" echo "Setting permissions"
ssh noisebridge@noisebell.local "chmod +x ~/noisebell " ssh noisebridge@noisebell.local "chmod +x ~/noisebell/noisebell"
echo "Deployment complete!" echo "Deployment complete!"

View file

@ -1,16 +1,4 @@
{ {
"endpoints": [ "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"
}
] ]
} }

View file

@ -3,8 +3,15 @@ mod webhook;
use std::time::Duration; use std::time::Duration;
use std::fs; use std::fs;
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use axum::{
routing::post,
Router,
Json,
extract::State,
};
use tracing::{error, info}; use tracing::{error, info};
use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
@ -12,8 +19,10 @@ use tracing_subscriber::filter::LevelFilter;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
info!("creating logs directory");
fs::create_dir_all("logs")?; fs::create_dir_all("logs")?;
info!("initializing logging");
let file_appender = RollingFileAppender::builder() let file_appender = RollingFileAppender::builder()
.rotation(Rotation::DAILY) .rotation(Rotation::DAILY)
.filename_prefix("noisebell") .filename_prefix("noisebell")
@ -35,23 +44,26 @@ async fn main() -> Result<()> {
.with(fmt::Layer::default().with_writer(non_blocking)) .with(fmt::Layer::default().with_writer(non_blocking))
.init(); .init();
info!("Starting noisebell...");
const DEFAULT_GPIO_PIN: u8 = 17; const DEFAULT_GPIO_PIN: u8 = 17;
const DEFAULT_WEBHOOK_RETRIES: u32 = 3; const DEFAULT_WEBHOOK_RETRIES: u32 = 3;
const DEFAULT_SERVER_PORT: u16 = 8080;
let gpio_pin = std::env::var("GPIO_PIN") info!("initializing webhook notifier");
.ok() let webhook_notifier = Arc::new(webhook::WebhookNotifier::new(DEFAULT_WEBHOOK_RETRIES)?);
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_GPIO_PIN);
let webhook_retries = std::env::var("WEBHOOK_RETRIES") info!("initializing gpio monitor");
.ok() let mut gpio_monitor = gpio::GpioMonitor::new(DEFAULT_GPIO_PIN, Duration::from_millis(100))?;
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_WEBHOOK_RETRIES);
let webhook_notifier = webhook::WebhookNotifier::new(webhook_retries)?; let app = Router::new()
let mut gpio_monitor = gpio::GpioMonitor::new(gpio_pin, Duration::from_millis(100))?; .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| { let callback = move |event: gpio::CircuitEvent| {
info!("Circuit state changed: {:?}", event); 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 { if let Err(e) = gpio_monitor.monitor(callback).await {
error!("GPIO monitoring error: {}", e); error!("GPIO monitoring error: {}", e);
@ -71,3 +83,15 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
async fn add_endpoint(
State(notifier): State<Arc<webhook::WebhookNotifier>>,
Json(endpoint): Json<webhook::Endpoint>,
) -> Result<(), axum::http::StatusCode> {
notifier.add_endpoint(endpoint)
.await
.map_err(|e| {
error!("Failed to add endpoint: {}", e);
axum::http::StatusCode::INTERNAL_SERVER_ERROR
})
}

View file

@ -2,19 +2,21 @@ use anyhow::Result;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info}; use tracing::{error, info};
use std::time::Duration; use std::time::Duration;
use futures::future::join_all; use futures::future::join_all;
use crate::gpio::CircuitEvent; use crate::gpio::CircuitEvent;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct Endpoint { pub struct Endpoint {
url: String, url: String,
description: String, description: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
struct EndpointsConfig { struct EndpointsConfig {
endpoints: Vec<Endpoint>, endpoints: Vec<Endpoint>,
} }
@ -29,22 +31,47 @@ struct WebhookPayload {
#[derive(Clone)] #[derive(Clone)]
pub struct WebhookNotifier { pub struct WebhookNotifier {
client: Client, client: Client,
endpoints: Vec<Endpoint>, endpoints: Arc<RwLock<Vec<Endpoint>>>,
max_retries: u32, max_retries: u32,
} }
impl WebhookNotifier { impl WebhookNotifier {
pub fn new(max_retries: u32) -> Result<Self> { pub fn new(max_retries: u32) -> Result<Self> {
let config = fs::read_to_string("endpoints.json")?; let endpoints = if let Ok(config) = fs::read_to_string("endpoints.json") {
let endpoints_config: EndpointsConfig = serde_json::from_str(&config)?; let endpoints_config: EndpointsConfig = serde_json::from_str(&config)?;
endpoints_config.endpoints
} else {
Vec::new()
};
Ok(Self { Ok(Self {
client: Client::new(), client: Client::new(),
endpoints: endpoints_config.endpoints, endpoints: Arc::new(RwLock::new(endpoints)),
max_retries, 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<()> { async fn send_webhook(&self, endpoint: &Endpoint, payload: &WebhookPayload) -> Result<()> {
match self.client match self.client
.post(&endpoint.url) .post(&endpoint.url)
@ -106,7 +133,8 @@ impl WebhookNotifier {
new_state: state.to_string(), 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| { .map(|endpoint| {
info!("Sending webhook to {}: {}", endpoint.description, serde_json::to_string(&payload).unwrap()); info!("Sending webhook to {}: {}", endpoint.description, serde_json::to_string(&payload).unwrap());
self.send_webhook_with_retries(endpoint, &payload) self.send_webhook_with_retries(endpoint, &payload)