diff --git a/remote/cache-service/assets/closed.png b/remote/cache-service/assets/closed.png new file mode 100644 index 0000000..a5a48c6 Binary files /dev/null and b/remote/cache-service/assets/closed.png differ diff --git a/remote/cache-service/assets/offline.png b/remote/cache-service/assets/offline.png new file mode 100644 index 0000000..5233b45 Binary files /dev/null and b/remote/cache-service/assets/offline.png differ diff --git a/remote/cache-service/assets/open.png b/remote/cache-service/assets/open.png new file mode 100644 index 0000000..515f2a8 Binary files /dev/null and b/remote/cache-service/assets/open.png differ diff --git a/remote/cache-service/src/api.rs b/remote/cache-service/src/api.rs index 4a50a48..326869d 100644 --- a/remote/cache-service/src/api.rs +++ b/remote/cache-service/src/api.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use axum::extract::State; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, StatusCode, header}; +use axum::response::IntoResponse; use axum::Json; use noisebell_common::{validate_bearer, HistoryEntry, WebhookPayload}; use tokio::sync::Mutex; @@ -11,6 +12,10 @@ use crate::db; use crate::types::{DoorStatus, WebhookTarget}; use crate::webhook; +static OPEN_PNG: &[u8] = include_bytes!("../assets/open.png"); +static CLOSED_PNG: &[u8] = include_bytes!("../assets/closed.png"); +static OFFLINE_PNG: &[u8] = include_bytes!("../assets/offline.png"); + pub struct AppState { pub db: Arc>, pub client: reqwest::Client, @@ -126,3 +131,86 @@ pub async fn get_history( Ok(Json(entries)) } + +pub async fn get_image_open() -> impl IntoResponse { + ([(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")], CLOSED_PNG) +} + +pub async fn get_image_offline() -> impl IntoResponse { + ([(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=86400")], OFFLINE_PNG) +} + +pub async fn get_image(State(state): State>) -> impl IntoResponse { + let db = state.db.clone(); + let status = tokio::task::spawn_blocking(move || { + let conn = db.blocking_lock(); + db::get_current_status_str(&conn) + }) + .await + .expect("db task panicked") + .ok() + .flatten(); + + let image = match status.as_deref() { + Some("open") => OPEN_PNG, + Some("closed") => CLOSED_PNG, + _ => OFFLINE_PNG, + }; + ([(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=5")], image) +} + +pub async fn get_badge(State(state): State>) -> impl IntoResponse { + let db = state.db.clone(); + let status = tokio::task::spawn_blocking(move || { + let conn = db.blocking_lock(); + db::get_current_status_str(&conn) + }) + .await + .expect("db task panicked") + .ok() + .flatten(); + + let (label, color) = match status.as_deref() { + Some("open") => ("open", "#57f287"), + Some("closed") => ("closed", "#ed4245"), + _ => ("offline", "#99aab5"), + }; + + let label_width = 70u32; + let value_width = 10 + label.len() as u32 * 7; + let total_width = label_width + value_width; + let label_x = label_width as f32 / 2.0; + let value_x = label_width as f32 + value_width as f32 / 2.0; + + let svg = format!( + "\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + noisebell\ + noisebell\ + {label}\ + {label}\ + " + ); + + ( + [ + (header::CONTENT_TYPE, "image/svg+xml"), + (header::CACHE_CONTROL, "no-cache, max-age=0"), + ], + svg, + ) +} diff --git a/remote/cache-service/src/main.rs b/remote/cache-service/src/main.rs index d2dc86d..8b25931 100644 --- a/remote/cache-service/src/main.rs +++ b/remote/cache-service/src/main.rs @@ -130,6 +130,11 @@ async fn main() -> Result<()> { .route("/status", get(api::get_status)) .route("/info", get(api::get_info)) .route("/history", get(api::get_history)) + .route("/image", get(api::get_image)) + .route("/image/open.png", get(api::get_image_open)) + .route("/image/closed.png", get(api::get_image_closed)) + .route("/image/offline.png", get(api::get_image_offline)) + .route("/badge.svg", get(api::get_badge)) .layer( TraceLayer::new_for_http() .make_span_with(tower_http::trace::DefaultMakeSpan::new().level(Level::INFO)) diff --git a/remote/discord-bot/src/main.rs b/remote/discord-bot/src/main.rs index b375293..d483d78 100644 --- a/remote/discord-bot/src/main.rs +++ b/remote/discord-bot/src/main.rs @@ -18,16 +18,17 @@ struct AppState { } fn build_embed(status: &str, timestamp: u64) -> CreateEmbed { - let (colour, title, description) = match status { - "open" => (Colour::from_rgb(87, 242, 135), "Door is open", "The door at Noisebridge is open."), - "closed" => (Colour::from_rgb(237, 66, 69), "Door is closed", "The door at Noisebridge is closed."), - _ => (Colour::from_rgb(153, 170, 181), "Pi is offline", "The Noisebridge Pi is offline."), + let (colour, title, description, image_url) = match status { + "open" => (Colour::from_rgb(0, 255, 0), "Noisebridge is Open!", "It's time to start hacking.", "https://noisebell.extremist.software/image/open.png"), + "closed" => (Colour::from_rgb(255, 0, 0), "Noisebridge is Closed!", "We'll see you again soon.", "https://noisebell.extremist.software/image/closed.png"), + _ => (Colour::from_rgb(153, 170, 181), "Noisebridge is Offline", "The Noisebridge Pi is not responding.", "https://noisebell.extremist.software/image/offline.png"), }; CreateEmbed::new() .title(title) .description(description) .colour(colour) + .thumbnail(image_url) .timestamp(serenity::model::Timestamp::from_unix_timestamp(timestamp as i64).unwrap_or_else(|_| serenity::model::Timestamp::now())) }