From 452b8b49c3898911953e7ad60c8b3ba8bea343ec Mon Sep 17 00:00:00 2001 From: Jet Date: Mon, 23 Mar 2026 13:55:52 -0700 Subject: [PATCH] feat: add basic rss feat support --- Cargo.toml | 10 ++ pi/pi-service/Cargo.toml | 3 + pi/pi-service/src/main.rs | 65 +++------- remote/cache-service/Cargo.toml | 3 + remote/cache-service/src/api.rs | 198 ++++++++++++++++++++++------- remote/cache-service/src/db.rs | 68 ++++------ remote/cache-service/src/main.rs | 7 + remote/cache-service/src/poller.rs | 10 +- remote/discord-bot/Cargo.toml | 3 + remote/discord-bot/src/main.rs | 16 +-- remote/noisebell-common/Cargo.toml | 3 + remote/noisebell-common/src/lib.rs | 10 +- rustfmt.toml | 2 + 13 files changed, 232 insertions(+), 166 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.toml b/Cargo.toml index d4c147b..207e891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,13 @@ members = [ "remote/discord-bot", ] resolver = "2" + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +dbg_macro = "warn" +print_stderr = "warn" +print_stdout = "warn" +todo = "warn" +unimplemented = "warn" diff --git a/pi/pi-service/Cargo.toml b/pi/pi-service/Cargo.toml index 10f8bf7..b1d3c8e 100644 --- a/pi/pi-service/Cargo.toml +++ b/pi/pi-service/Cargo.toml @@ -3,6 +3,9 @@ name = "noisebell" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] anyhow = "1.0" axum = "0.8" diff --git a/pi/pi-service/src/main.rs b/pi/pi-service/src/main.rs index ca984cf..5d42274 100644 --- a/pi/pi-service/src/main.rs +++ b/pi/pi-service/src/main.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicU8, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -42,7 +42,6 @@ impl LocalDoorState { Self::Closed => DoorStatus::Closed, } } - } struct AppState { @@ -58,10 +57,7 @@ impl AppState { } fn unix_timestamp() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() } async fn get_status( @@ -124,11 +120,7 @@ async fn main() -> Result<()> { .unwrap_or_else(|_| "true".into()) .parse() .context("NOISEBELL_ACTIVE_LOW must be true or false")?; - let active_level = if active_low { - SignalLevel::Low - } else { - SignalLevel::High - }; + let active_level = if active_low { SignalLevel::Low } else { SignalLevel::High }; let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY") .context("NOISEBELL_INBOUND_API_KEY is required")?; @@ -137,33 +129,20 @@ async fn main() -> Result<()> { let chip = Chip::new("gpiochip0").context("failed to open gpiochip0")?; - let bias = if active_level == SignalLevel::Low { - Bias::PullUp - } else { - Bias::PullDown - }; + let bias = if active_level == SignalLevel::Low { Bias::PullUp } else { Bias::PullDown }; // Keep the line requested and poll its value. Edge-triggered reads have // proven unreliable on Raspberry Pi OS even though the raw line level is // correct, so we debounce from sampled levels instead. - let opts = Options::input([gpio_pin]) - .bias(bias) - .consumer("noisebell"); - let inputs = chip - .request_lines(opts) - .context(format!("failed to request GPIO line {gpio_pin}"))?; + let opts = Options::input([gpio_pin]).bias(bias).consumer("noisebell"); + let inputs = + chip.request_lines(opts).context(format!("failed to request GPIO line {gpio_pin}"))?; // Read initial value - let initial_values = inputs - .get_values([false]) - .context("failed to read initial GPIO value")?; + let initial_values = inputs.get_values([false]).context("failed to read initial GPIO value")?; // Value is true when line is active. With Active::High (default), // true means the physical level is high. - let initial_raw_level = if initial_values[0] { - SignalLevel::High - } else { - SignalLevel::Low - }; + let initial_raw_level = if initial_values[0] { SignalLevel::High } else { SignalLevel::Low }; let initial_state = LocalDoorState::from_raw_level(initial_raw_level, active_level); let now = unix_timestamp(); @@ -205,11 +184,7 @@ async fn main() -> Result<()> { } }; - let new_raw_level = if values[0] { - SignalLevel::High - } else { - SignalLevel::Low - }; + let new_raw_level = if values[0] { SignalLevel::High } else { SignalLevel::Low }; let new_state = LocalDoorState::from_raw_level(new_raw_level, active_level); if new_state != pending_state { @@ -218,9 +193,7 @@ async fn main() -> Result<()> { } else if new_state != current_state && pending_since.elapsed() >= debounce { current_state = new_state; let previous_state = LocalDoorState::from_atomic( - state_for_edges - .door_state - .swap(new_state as u8, Ordering::Relaxed), + state_for_edges.door_state.swap(new_state as u8, Ordering::Relaxed), ); if previous_state == new_state { @@ -229,9 +202,7 @@ async fn main() -> Result<()> { } let timestamp = unix_timestamp(); - state_for_edges - .last_changed - .store(timestamp, Ordering::Relaxed); + state_for_edges.last_changed.store(timestamp, Ordering::Relaxed); let _ = edge_tx.send((new_state.as_door_status(), timestamp)); } @@ -252,12 +223,8 @@ async fn main() -> Result<()> { let payload = WebhookPayload { status, timestamp }; for attempt in 0..=retry_attempts { - let result = client - .post(&endpoint_url) - .bearer_auth(&api_key) - .json(&payload) - .send() - .await; + let result = + client.post(&endpoint_url).bearer_auth(&api_key).json(&payload).send().await; match result { Ok(resp) if resp.status().is_success() => break, _ => { @@ -279,9 +246,7 @@ async fn main() -> Result<()> { } }); - let app = Router::new() - .route("/", get(get_status)) - .with_state(state); + let app = Router::new().route("/", get(get_status)).with_state(state); let listener = tokio::net::TcpListener::bind((&*bind_address, port)) .await diff --git a/remote/cache-service/Cargo.toml b/remote/cache-service/Cargo.toml index ece06ae..d375e79 100644 --- a/remote/cache-service/Cargo.toml +++ b/remote/cache-service/Cargo.toml @@ -3,6 +3,9 @@ name = "noisebell-cache" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] anyhow = "1.0" axum = "0.8" diff --git a/remote/cache-service/src/api.rs b/remote/cache-service/src/api.rs index e0b4450..2ee7d34 100644 --- a/remote/cache-service/src/api.rs +++ b/remote/cache-service/src/api.rs @@ -22,6 +22,7 @@ pub struct AppState { pub db: Arc>, pub client: reqwest::Client, pub inbound_api_key: String, + pub public_base_url: Option, pub webhooks: Vec, pub retry_attempts: u32, pub retry_base_delay_secs: u64, @@ -30,10 +31,7 @@ pub struct AppState { } fn unix_now() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() } fn format_full_timestamp(ts: u64) -> String { @@ -42,13 +40,92 @@ fn format_full_timestamp(ts: u64) -> String { .unwrap_or_else(|| format!("unix timestamp {ts}")) } +fn format_rfc2822_timestamp(ts: u64) -> String { + DateTime::from_timestamp(ts as i64, 0) + .map(|dt: DateTime| dt.to_rfc2822()) + .unwrap_or_else(|| "Thu, 01 Jan 1970 00:00:00 +0000".to_string()) +} + +fn xml_escape(text: &str) -> String { + let mut escaped = String::with_capacity(text.len()); + for ch in text.chars() { + match ch { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '"' => escaped.push_str("""), + '\'' => escaped.push_str("'"), + _ => escaped.push(ch), + } + } + escaped +} + +fn header_value(headers: &HeaderMap, name: &'static str) -> Option { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) +} + +fn public_base_url(state: &AppState, headers: &HeaderMap) -> String { + if let Some(url) = &state.public_base_url { + return url.clone(); + } + + let host = header_value(headers, "x-forwarded-host") + .or_else(|| header_value(headers, "host")) + .unwrap_or_else(|| "localhost:3000".to_string()); + let scheme = header_value(headers, "x-forwarded-proto").unwrap_or_else(|| "http".to_string()); + format!("{scheme}://{host}") +} + +fn build_rss_feed(base_url: &str, status: &CacheStatusResponse) -> String { + let item_timestamp = status.since.or(status.last_checked).unwrap_or(0); + let pub_date = format_rfc2822_timestamp(item_timestamp); + let feed_url = format!("{base_url}/rss.xml"); + let status_url = format!("{base_url}/status"); + let guid = format!("urn:noisebell:status:{}:{item_timestamp}", status.status.as_str()); + let title = format!("Noisebell is {}", status.status); + let description = if status.human_readable.is_empty() { + format!("Current status: {}.", status.status) + } else { + status.human_readable.clone() + }; + + format!( + concat!( + "\n", + "\n", + " \n", + " Noisebell status\n", + " {channel_link}\n", + " Current noisebell state as a single rolling RSS item.\n", + " {pub_date}\n", + " 5\n", + " \n", + " {item_title}\n", + " {item_link}\n", + " {item_guid}\n", + " {pub_date}\n", + " {item_description}\n", + " \n", + " \n", + "\n" + ), + channel_link = xml_escape(&feed_url), + pub_date = xml_escape(&pub_date), + item_title = xml_escape(&title), + item_link = xml_escape(&status_url), + item_guid = xml_escape(&guid), + item_description = xml_escape(&description), + ) +} + fn format_duration(seconds: u64) -> String { - let units = [ - (86_400, "day"), - (3_600, "hour"), - (60, "minute"), - (1, "second"), - ]; + let units = [(86_400, "day"), (3_600, "hour"), (60, "minute"), (1, "second")]; let mut remaining = seconds; let mut parts = Vec::new(); @@ -75,7 +152,12 @@ fn format_duration(seconds: u64) -> String { } } -fn status_summary(status: DoorStatus, since: Option, last_checked: Option, now: u64) -> String { +fn status_summary( + status: DoorStatus, + since: Option, + last_checked: Option, + now: u64, +) -> String { let since_text = since .map(|ts| { format!( @@ -111,16 +193,10 @@ pub async fn post_webhook( // Simple rate limiting: reset tokens every window, reject if exhausted. let now = unix_now(); - let last = state - .webhook_last_request - .load(std::sync::atomic::Ordering::Relaxed); + let last = state.webhook_last_request.load(std::sync::atomic::Ordering::Relaxed); if now.saturating_sub(last) >= WEBHOOK_RATE_WINDOW_SECS { - state - .webhook_tokens - .store(WEBHOOK_RATE_LIMIT, std::sync::atomic::Ordering::Relaxed); - state - .webhook_last_request - .store(now, std::sync::atomic::Ordering::Relaxed); + state.webhook_tokens.store(WEBHOOK_RATE_LIMIT, std::sync::atomic::Ordering::Relaxed); + state.webhook_last_request.store(now, std::sync::atomic::Ordering::Relaxed); } let remaining = state.webhook_tokens.fetch_update( std::sync::atomic::Ordering::Relaxed, @@ -153,10 +229,7 @@ pub async fn post_webhook( webhook::forward( &state.client, &state.webhooks, - &WebhookPayload { - status, - timestamp: body.timestamp, - }, + &WebhookPayload { status, timestamp: body.timestamp }, state.retry_attempts, state.retry_base_delay_secs, ) @@ -200,41 +273,63 @@ pub async fn get_status( StatusCode::INTERNAL_SERVER_ERROR })?; - status.human_readable = status_summary(status.status, status.since, status.last_checked, unix_now()); + status.human_readable = + status_summary(status.status, status.since, status.last_checked, unix_now()); Ok(Json(status)) } +pub async fn get_rss( + State(state): State>, + headers: HeaderMap, +) -> Result { + let db = state.db.clone(); + let mut status = tokio::task::spawn_blocking(move || { + let conn = db.blocking_lock(); + db::get_status(&conn) + }) + .await + .expect("db task panicked") + .map_err(|e| { + error!(error = %e, "failed to get status for rss"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + status.human_readable = + status_summary(status.status, status.since, status.last_checked, unix_now()); + let base_url = public_base_url(&state, &headers); + let feed = build_rss_feed(&base_url, &status); + + Ok(( + [ + (header::CONTENT_TYPE, "application/rss+xml; charset=utf-8"), + (header::CACHE_CONTROL, "public, max-age=60"), + ], + feed, + )) +} + pub async fn health() -> StatusCode { StatusCode::OK } pub async fn get_image_open() -> impl IntoResponse { ( - [ - (header::CONTENT_TYPE, "image/png"), - (header::CACHE_CONTROL, "public, max-age=86400"), - ], + [(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=86400")], OPEN_PNG, ) } pub async fn get_image_closed() -> impl IntoResponse { ( - [ - (header::CONTENT_TYPE, "image/png"), - (header::CACHE_CONTROL, "public, max-age=86400"), - ], + [(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=86400")], CLOSED_PNG, ) } pub async fn get_image_offline() -> impl IntoResponse { ( - [ - (header::CONTENT_TYPE, "image/png"), - (header::CACHE_CONTROL, "public, max-age=86400"), - ], + [(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=86400")], OFFLINE_PNG, ) } @@ -260,13 +355,7 @@ pub async fn get_image(State(state): State>) -> Response { DoorStatus::Closed => CLOSED_PNG, DoorStatus::Offline => OFFLINE_PNG, }; - ( - [ - (header::CONTENT_TYPE, "image/png"), - (header::CACHE_CONTROL, "public, max-age=5"), - ], - image, - ) + ([(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=5")], image) .into_response() } @@ -290,4 +379,25 @@ mod tests { assert!(summary.contains("Last checked")); assert!(summary.contains("55 seconds ago")); } + + #[test] + fn rss_feed_uses_single_current_item() { + let feed = build_rss_feed( + "https://noisebell.example.com", + &CacheStatusResponse { + status: DoorStatus::Closed, + since: Some(1_700_000_000), + last_checked: Some(1_700_000_120), + human_readable: + "We've been closed since Tuesday, November 14, 2023 at 10:13:20 PM UTC." + .to_string(), + }, + ); + + assert!(feed.contains("Noisebell is closed")); + assert!(feed + .contains("urn:noisebell:status:closed:1700000000")); + assert!(feed.contains("https://noisebell.example.com/status")); + assert_eq!(feed.matches("").count(), 1); + } } diff --git a/remote/cache-service/src/db.rs b/remote/cache-service/src/db.rs index c5449e1..0f14145 100644 --- a/remote/cache-service/src/db.rs +++ b/remote/cache-service/src/db.rs @@ -1,6 +1,3 @@ -use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; - use anyhow::{Context, Result}; use noisebell_common::{CacheStatusResponse, DoorStatus}; use rusqlite::{Connection, OptionalExtension}; @@ -63,9 +60,7 @@ struct CurrentStateRow { } fn parse_status(status: &str, location: &str) -> Result { - status - .parse() - .with_context(|| format!("invalid door status {status:?} in {location}")) + status.parse().with_context(|| format!("invalid door status {status:?} in {location}")) } pub fn init(path: &str) -> Result { @@ -103,18 +98,12 @@ fn current_state_has_column(conn: &Connection, column: &str) -> Result { fn migrate_current_state(conn: &Connection) -> Result<()> { if !current_state_has_column(conn, "last_checked")? { - conn.execute( - "ALTER TABLE current_state ADD COLUMN last_checked INTEGER", - [], - ) - .context("failed to add current_state.last_checked")?; + conn.execute("ALTER TABLE current_state ADD COLUMN last_checked INTEGER", []) + .context("failed to add current_state.last_checked")?; } - conn.execute( - "UPDATE current_state SET status = 'offline' WHERE status IS NULL", - [], - ) - .context("failed to backfill NULL current_state.status")?; + conn.execute("UPDATE current_state SET status = 'offline' WHERE status IS NULL", []) + .context("failed to backfill NULL current_state.status")?; validate_status_values(conn)?; Ok(()) @@ -168,14 +157,12 @@ fn current_state_row(conn: &Connection) -> Result { let status = parse_status(&status_str, "current_state.status")?; let state = match (status, since) { - (DoorStatus::Open, Some(since)) => CachedState::Live { - status: LiveDoorStatus::Open, - since, - }, - (DoorStatus::Closed, Some(since)) => CachedState::Live { - status: LiveDoorStatus::Closed, - since, - }, + (DoorStatus::Open, Some(since)) => { + CachedState::Live { status: LiveDoorStatus::Open, since } + } + (DoorStatus::Closed, Some(since)) => { + CachedState::Live { status: LiveDoorStatus::Closed, since } + } (DoorStatus::Offline, Some(since)) => CachedState::Offline { since }, (DoorStatus::Offline, None) => CachedState::Unknown, (DoorStatus::Open | DoorStatus::Closed, None) => { @@ -183,11 +170,7 @@ fn current_state_row(conn: &Connection) -> Result { } }; - Ok(CurrentStateRow { - state, - last_seen, - last_checked, - }) + Ok(CurrentStateRow { state, last_seen, last_checked }) } pub fn get_status(conn: &Connection) -> Result { @@ -228,10 +211,11 @@ pub fn apply_state( CachedState::Offline { since } if timestamp < since => ApplyStateOutcome::Stale, CachedState::Offline { .. } => ApplyStateOutcome::Applied, CachedState::Live { status: _, since } if timestamp < since => ApplyStateOutcome::Stale, - CachedState::Live { - status: current_status, - since, - } if timestamp == since && live_status == current_status => ApplyStateOutcome::Duplicate, + CachedState::Live { status: current_status, since } + if timestamp == since && live_status == current_status => + { + ApplyStateOutcome::Duplicate + } CachedState::Live { .. } => ApplyStateOutcome::Applied, }; @@ -244,10 +228,7 @@ pub fn apply_state( } pub fn update_last_seen(conn: &Connection, now: u64) -> Result<()> { - conn.execute( - "UPDATE current_state SET last_seen = ?1 WHERE id = 1", - rusqlite::params![now], - )?; + conn.execute("UPDATE current_state SET last_seen = ?1 WHERE id = 1", rusqlite::params![now])?; Ok(()) } @@ -275,16 +256,15 @@ pub fn get_current_status(conn: &Connection) -> Result { #[cfg(test)] mod tests { use super::*; + use std::path::Path; + use std::time::{SystemTime, UNIX_EPOCH}; fn test_db() -> Connection { init(":memory:").expect("failed to init test db") } fn temp_db_path(label: &str) -> std::path::PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); + let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); std::env::temp_dir().join(format!("noisebell-{label}-{nanos}.sqlite")) } @@ -464,11 +444,7 @@ mod tests { create_legacy_db(&path); let conn = Connection::open(&path).unwrap(); - conn.execute( - "UPDATE current_state SET status = 'mystery' WHERE id = 1", - [], - ) - .unwrap(); + conn.execute("UPDATE current_state SET status = 'mystery' WHERE id = 1", []).unwrap(); drop(conn); let err = init(path.to_str().unwrap()).unwrap_err().to_string(); diff --git a/remote/cache-service/src/main.rs b/remote/cache-service/src/main.rs index dae7927..ef65872 100644 --- a/remote/cache-service/src/main.rs +++ b/remote/cache-service/src/main.rs @@ -39,6 +39,11 @@ async fn main() -> Result<()> { let inbound_api_key = std::env::var("NOISEBELL_CACHE_INBOUND_API_KEY") .context("NOISEBELL_CACHE_INBOUND_API_KEY is required")?; + let public_base_url = std::env::var("NOISEBELL_CACHE_PUBLIC_BASE_URL") + .ok() + .map(|url| url.trim_end_matches('/').to_string()) + .filter(|url| !url.is_empty()); + let data_dir = std::env::var("NOISEBELL_CACHE_DATA_DIR") .unwrap_or_else(|_| "/var/lib/noisebell-cache".into()); @@ -116,6 +121,7 @@ async fn main() -> Result<()> { db, client, inbound_api_key, + public_base_url, webhooks, retry_attempts, retry_base_delay_secs, @@ -127,6 +133,7 @@ async fn main() -> Result<()> { .route("/health", get(api::health)) .route("/webhook", post(api::post_webhook)) .route("/status", get(api::get_status)) + .route("/rss.xml", get(api::get_rss)) .route("/image", get(api::get_image)) .route("/image/open.png", get(api::get_image_open)) .route("/image/closed.png", get(api::get_image_closed)) diff --git a/remote/cache-service/src/poller.rs b/remote/cache-service/src/poller.rs index 2a20213..df849ae 100644 --- a/remote/cache-service/src/poller.rs +++ b/remote/cache-service/src/poller.rs @@ -21,10 +21,7 @@ pub struct PollerConfig { } fn unix_now() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() } pub fn spawn_status_poller( @@ -168,10 +165,7 @@ pub fn spawn_status_poller( webhook::forward( &client, &config.webhooks, - &WebhookPayload { - status: DoorStatus::Offline, - timestamp: now, - }, + &WebhookPayload { status: DoorStatus::Offline, timestamp: now }, config.retry_attempts, config.retry_base_delay_secs, ) diff --git a/remote/discord-bot/Cargo.toml b/remote/discord-bot/Cargo.toml index 3dd4a51..28e08bb 100644 --- a/remote/discord-bot/Cargo.toml +++ b/remote/discord-bot/Cargo.toml @@ -3,6 +3,9 @@ name = "noisebell-discord" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] anyhow = "1.0" axum = "0.8" diff --git a/remote/discord-bot/src/main.rs b/remote/discord-bot/src/main.rs index 744ddec..838148f 100644 --- a/remote/discord-bot/src/main.rs +++ b/remote/discord-bot/src/main.rs @@ -87,10 +87,7 @@ async fn post_webhook( } fn unix_now() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() } fn format_timestamp(ts: u64) -> String { @@ -106,11 +103,9 @@ async fn handle_status( let embed = match resp { Ok(resp) if resp.status().is_success() => match resp.json::().await { - Ok(data) => build_embed( - data.status, - data.since.unwrap_or(unix_now()), - &state.image_base_url, - ), + Ok(data) => { + build_embed(data.status, data.since.unwrap_or(unix_now()), &state.image_base_url) + } Err(e) => { error!(error = %e, "failed to parse status response"); CreateEmbed::new() @@ -137,7 +132,8 @@ impl serenity::all::EventHandler for Handler { async fn ready(&self, ctx: serenity::all::Context, ready: serenity::model::gateway::Ready) { info!(user = %ready.user.name, "Discord bot connected"); - let commands = vec![CreateCommand::new("status").description("Show the current door status")]; + let commands = + vec![CreateCommand::new("status").description("Show the current door status")]; if let Err(e) = serenity::all::Command::set_global_commands(&ctx.http, commands).await { error!(error = %e, "failed to register slash commands"); diff --git a/remote/noisebell-common/Cargo.toml b/remote/noisebell-common/Cargo.toml index 6cb2d0e..aeda17a 100644 --- a/remote/noisebell-common/Cargo.toml +++ b/remote/noisebell-common/Cargo.toml @@ -3,6 +3,9 @@ name = "noisebell-common" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] axum = "0.8" serde = { version = "1.0", features = ["derive"] } diff --git a/remote/noisebell-common/src/lib.rs b/remote/noisebell-common/src/lib.rs index 6c977bb..21e381b 100644 --- a/remote/noisebell-common/src/lib.rs +++ b/remote/noisebell-common/src/lib.rs @@ -148,10 +148,7 @@ mod tests { fn door_status_round_trips() { for status in DoorStatus::ALL { assert_eq!(status.as_str().parse::().unwrap(), status); - assert_eq!( - serde_json::to_string(&status).unwrap(), - format!("\"{status}\"") - ); + assert_eq!(serde_json::to_string(&status).unwrap(), format!("\"{status}\"")); } } @@ -163,10 +160,7 @@ mod tests { #[test] fn webhook_payload_round_trips() { - let payload = WebhookPayload { - status: DoorStatus::Open, - timestamp: 1234567890, - }; + let payload = WebhookPayload { status: DoorStatus::Open, timestamp: 1234567890 }; let json = serde_json::to_string(&payload).unwrap(); let deserialized: WebhookPayload = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.status, DoorStatus::Open); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8635cf9 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 100 +use_small_heuristics = "Max"