feat: remove rss, status, and badge features
This commit is contained in:
parent
553d7d1780
commit
36720e2ba5
21 changed files with 904 additions and 1200 deletions
|
|
@ -1,15 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::http::{HeaderMap, StatusCode, header};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::http::{header, HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use noisebell_common::{validate_bearer, HistoryEntry, WebhookPayload};
|
||||
use noisebell_common::{validate_bearer, CacheStatusResponse, DoorStatus, WebhookPayload};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::db;
|
||||
use crate::types::{DoorStatus, WebhookTarget};
|
||||
use crate::db::ApplyStateOutcome;
|
||||
use crate::types::WebhookTarget;
|
||||
use crate::webhook;
|
||||
|
||||
static OPEN_PNG: &[u8] = include_bytes!("../assets/open.png");
|
||||
|
|
@ -46,12 +47,18 @@ pub async fn post_webhook(
|
|||
return StatusCode::UNAUTHORIZED;
|
||||
}
|
||||
|
||||
// Simple rate limiting: reset tokens every window, reject if exhausted
|
||||
// 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,
|
||||
|
|
@ -62,43 +69,63 @@ pub async fn post_webhook(
|
|||
return StatusCode::TOO_MANY_REQUESTS;
|
||||
}
|
||||
|
||||
let Some(status) = DoorStatus::from_str(&body.status) else {
|
||||
return StatusCode::BAD_REQUEST;
|
||||
};
|
||||
|
||||
let now = unix_now();
|
||||
let db = state.db.clone();
|
||||
let status = body.status;
|
||||
let timestamp = body.timestamp;
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
db::update_state(&conn, status, timestamp, now)
|
||||
db::apply_state(&conn, status, timestamp, now)
|
||||
})
|
||||
.await
|
||||
.expect("db task panicked");
|
||||
|
||||
if let Err(e) = result {
|
||||
error!(error = %e, "failed to update state from webhook");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
match result {
|
||||
Ok(ApplyStateOutcome::Applied) => {
|
||||
info!(
|
||||
status = %status,
|
||||
timestamp = body.timestamp,
|
||||
"state updated via webhook"
|
||||
);
|
||||
|
||||
webhook::forward(
|
||||
&state.client,
|
||||
&state.webhooks,
|
||||
&WebhookPayload {
|
||||
status,
|
||||
timestamp: body.timestamp,
|
||||
},
|
||||
state.retry_attempts,
|
||||
state.retry_base_delay_secs,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(ApplyStateOutcome::Duplicate) => {
|
||||
info!(
|
||||
status = %status,
|
||||
timestamp = body.timestamp,
|
||||
"duplicate webhook ignored"
|
||||
);
|
||||
}
|
||||
Ok(ApplyStateOutcome::Stale) => {
|
||||
info!(
|
||||
status = %status,
|
||||
timestamp = body.timestamp,
|
||||
"stale webhook ignored"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to update state from webhook");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
info!(status = status.as_str(), timestamp = body.timestamp, "state updated via webhook");
|
||||
|
||||
webhook::forward(
|
||||
&state.client,
|
||||
&state.webhooks,
|
||||
&WebhookPayload {
|
||||
status: status.as_str().to_string(),
|
||||
timestamp: body.timestamp,
|
||||
},
|
||||
state.retry_attempts,
|
||||
state.retry_base_delay_secs,
|
||||
)
|
||||
.await;
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
pub async fn get_status(State(state): State<Arc<AppState>>) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
pub async fn get_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<CacheStatusResponse>, StatusCode> {
|
||||
let db = state.db.clone();
|
||||
let status = tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
|
|
@ -111,125 +138,70 @@ pub async fn get_status(State(state): State<Arc<AppState>>) -> Result<Json<serde
|
|||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(serde_json::to_value(status).unwrap()))
|
||||
}
|
||||
|
||||
pub async fn get_info(State(state): State<Arc<AppState>>) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let db = state.db.clone();
|
||||
let info = tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
db::get_pi_info(&conn)
|
||||
})
|
||||
.await
|
||||
.expect("db task panicked")
|
||||
.map_err(|e| {
|
||||
error!(error = %e, "failed to get pi info");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(info))
|
||||
Ok(Json(status))
|
||||
}
|
||||
|
||||
pub async fn health() -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
pub async fn get_history(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<HistoryEntry>>, StatusCode> {
|
||||
let limit = 100u32;
|
||||
let db = state.db.clone();
|
||||
let entries = tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
db::get_history(&conn, limit)
|
||||
})
|
||||
.await
|
||||
.expect("db task panicked")
|
||||
.map_err(|e| {
|
||||
error!(error = %e, "failed to get history");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
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)
|
||||
(
|
||||
[
|
||||
(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)
|
||||
(
|
||||
[
|
||||
(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)
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400"),
|
||||
],
|
||||
OFFLINE_PNG,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_image(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
pub async fn get_image(State(state): State<Arc<AppState>>) -> Response {
|
||||
let db = state.db.clone();
|
||||
let status = tokio::task::spawn_blocking(move || {
|
||||
let status = match tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
db::get_current_status(&conn)
|
||||
})
|
||||
.await
|
||||
.expect("db task panicked")
|
||||
.unwrap_or(DoorStatus::Offline);
|
||||
{
|
||||
Ok(status) => status,
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to get current status for image");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let image = match status {
|
||||
DoorStatus::Open => OPEN_PNG,
|
||||
DoorStatus::Closed => CLOSED_PNG,
|
||||
DoorStatus::Offline => OFFLINE_PNG,
|
||||
};
|
||||
([(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "public, max-age=5")], image)
|
||||
}
|
||||
|
||||
pub async fn get_badge(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let db = state.db.clone();
|
||||
let status = tokio::task::spawn_blocking(move || {
|
||||
let conn = db.blocking_lock();
|
||||
db::get_current_status(&conn)
|
||||
})
|
||||
.await
|
||||
.expect("db task panicked")
|
||||
.unwrap_or(DoorStatus::Offline);
|
||||
|
||||
let (label, color) = match status {
|
||||
DoorStatus::Open => ("open", "#57f287"),
|
||||
DoorStatus::Closed => ("closed", "#ed4245"),
|
||||
DoorStatus::Offline => ("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!(
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{total_width}\" height=\"20\">\
|
||||
<linearGradient id=\"s\" x2=\"0\" y2=\"100%\">\
|
||||
<stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>\
|
||||
<stop offset=\"1\" stop-opacity=\".1\"/>\
|
||||
</linearGradient>\
|
||||
<clipPath id=\"r\"><rect width=\"{total_width}\" height=\"20\" rx=\"3\" fill=\"#fff\"/></clipPath>\
|
||||
<g clip-path=\"url(#r)\">\
|
||||
<rect width=\"{label_width}\" height=\"20\" fill=\"#555\"/>\
|
||||
<rect x=\"{label_width}\" width=\"{value_width}\" height=\"20\" fill=\"{color}\"/>\
|
||||
<rect width=\"{total_width}\" height=\"20\" fill=\"url(#s)\"/>\
|
||||
</g>\
|
||||
<g fill=\"#fff\" text-anchor=\"middle\" font-family=\"Verdana,Geneva,sans-serif\" font-size=\"11\">\
|
||||
<text x=\"{label_x}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">noisebell</text>\
|
||||
<text x=\"{label_x}\" y=\"14\">noisebell</text>\
|
||||
<text x=\"{value_x}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{label}</text>\
|
||||
<text x=\"{value_x}\" y=\"14\">{label}</text>\
|
||||
</g></svg>"
|
||||
);
|
||||
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/svg+xml"),
|
||||
(header::CACHE_CONTROL, "no-cache, max-age=0"),
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::CACHE_CONTROL, "public, max-age=5"),
|
||||
],
|
||||
svg,
|
||||
image,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue