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

View file

@ -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<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 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<Endpoint>,
}
@ -29,22 +31,47 @@ struct WebhookPayload {
#[derive(Clone)]
pub struct WebhookNotifier {
client: Client,
endpoints: Vec<Endpoint>,
endpoints: Arc<RwLock<Vec<Endpoint>>>,
max_retries: u32,
}
impl WebhookNotifier {
pub fn new(max_retries: u32) -> Result<Self> {
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)