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
|
|
@ -6,7 +6,7 @@ use axum::extract::State as AxumState;
|
|||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use noisebell_common::{validate_bearer, WebhookPayload};
|
||||
use noisebell_common::{validate_bearer, CacheStatusResponse, DoorStatus, WebhookPayload};
|
||||
use serenity::all::{
|
||||
ChannelId, Colour, CommandInteraction, CreateCommand, CreateEmbed, CreateInteractionResponse,
|
||||
CreateInteractionResponseMessage, CreateMessage, GatewayIntents, Interaction,
|
||||
|
|
@ -24,11 +24,26 @@ struct AppState {
|
|||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
fn build_embed(status: &str, timestamp: u64, image_base_url: &str) -> CreateEmbed {
|
||||
fn build_embed(status: DoorStatus, timestamp: u64, image_base_url: &str) -> CreateEmbed {
|
||||
let (colour, title, description, image_file) = match status {
|
||||
"open" => (Colour::from_rgb(0, 255, 0), "Noisebridge is Open!", "It's time to start hacking.", "open.png"),
|
||||
"closed" => (Colour::from_rgb(255, 0, 0), "Noisebridge is Closed!", "We'll see you again soon.", "closed.png"),
|
||||
_ => (Colour::from_rgb(153, 170, 181), "Noisebridge is Offline", "The Noisebridge Pi is not responding.", "offline.png"),
|
||||
DoorStatus::Open => (
|
||||
Colour::from_rgb(0, 255, 0),
|
||||
"Noisebridge is Open!",
|
||||
"It's time to start hacking.",
|
||||
"open.png",
|
||||
),
|
||||
DoorStatus::Closed => (
|
||||
Colour::from_rgb(255, 0, 0),
|
||||
"Noisebridge is Closed!",
|
||||
"We'll see you again soon.",
|
||||
"closed.png",
|
||||
),
|
||||
DoorStatus::Offline => (
|
||||
Colour::from_rgb(153, 170, 181),
|
||||
"Noisebridge is Offline",
|
||||
"The Noisebridge Pi is not responding.",
|
||||
"offline.png",
|
||||
),
|
||||
};
|
||||
|
||||
let image_url = format!("{image_base_url}/{image_file}");
|
||||
|
|
@ -38,7 +53,10 @@ fn build_embed(status: &str, timestamp: u64, image_base_url: &str) -> CreateEmbe
|
|||
.description(description)
|
||||
.colour(colour)
|
||||
.thumbnail(image_url)
|
||||
.timestamp(serenity::model::Timestamp::from_unix_timestamp(timestamp as i64).unwrap_or_else(|_| serenity::model::Timestamp::now()))
|
||||
.timestamp(
|
||||
serenity::model::Timestamp::from_unix_timestamp(timestamp as i64)
|
||||
.unwrap_or_else(|_| serenity::model::Timestamp::now()),
|
||||
)
|
||||
}
|
||||
|
||||
async fn post_webhook(
|
||||
|
|
@ -52,7 +70,7 @@ async fn post_webhook(
|
|||
|
||||
info!(status = %body.status, timestamp = body.timestamp, "received webhook");
|
||||
|
||||
let embed = build_embed(&body.status, body.timestamp, &state.image_base_url);
|
||||
let embed = build_embed(body.status, body.timestamp, &state.image_base_url);
|
||||
let message = CreateMessage::new().embed(embed);
|
||||
|
||||
match state.channel_id.send_message(&state.http, message).await {
|
||||
|
|
@ -78,173 +96,49 @@ fn format_timestamp(ts: u64) -> String {
|
|||
format!("<t:{}:R>", ts)
|
||||
}
|
||||
|
||||
async fn handle_status(state: &AppState, _command: &CommandInteraction) -> CreateInteractionResponse {
|
||||
async fn handle_status(
|
||||
state: &AppState,
|
||||
_command: &CommandInteraction,
|
||||
) -> CreateInteractionResponse {
|
||||
let url = format!("{}/status", state.cache_url);
|
||||
let resp = state.client.get(&url).send().await;
|
||||
|
||||
let embed = match resp {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<serde_json::Value>().await {
|
||||
Ok(data) => {
|
||||
let status = data.get("status").and_then(|s| s.as_str()).unwrap_or("unknown");
|
||||
let since = data.get("since").and_then(|t| t.as_u64());
|
||||
let last_checked = data.get("last_checked").and_then(|t| t.as_u64());
|
||||
Ok(resp) if resp.status().is_success() => match resp.json::<CacheStatusResponse>().await {
|
||||
Ok(data) => {
|
||||
let mut embed = build_embed(
|
||||
data.status,
|
||||
data.since.unwrap_or(unix_now()),
|
||||
&state.image_base_url,
|
||||
);
|
||||
|
||||
let mut embed = build_embed(status, since.unwrap_or(unix_now()), &state.image_base_url);
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if let Some(ts) = since {
|
||||
fields.push(("Since", format_timestamp(ts), true));
|
||||
}
|
||||
if let Some(ts) = last_checked {
|
||||
fields.push(("Last Checked", format_timestamp(ts), true));
|
||||
}
|
||||
if !fields.is_empty() {
|
||||
embed = embed.fields(fields);
|
||||
}
|
||||
embed
|
||||
let mut fields = Vec::new();
|
||||
if let Some(ts) = data.since {
|
||||
fields.push(("Since", format_timestamp(ts), true));
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to parse status response");
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to parse status response.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
if let Some(ts) = data.last_checked {
|
||||
fields.push(("Last Checked", format_timestamp(ts), true));
|
||||
}
|
||||
if !fields.is_empty() {
|
||||
embed = embed.fields(fields);
|
||||
}
|
||||
embed
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to reach the cache service.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to parse status response");
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to parse status response.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
},
|
||||
_ => CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to reach the cache service.")
|
||||
.colour(Colour::from_rgb(255, 0, 0)),
|
||||
};
|
||||
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new().embed(embed)
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_info(state: &AppState, _command: &CommandInteraction) -> CreateInteractionResponse {
|
||||
let url = format!("{}/info", state.cache_url);
|
||||
let resp = state.client.get(&url).send().await;
|
||||
|
||||
let embed = match resp {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<serde_json::Value>().await {
|
||||
Ok(data) => {
|
||||
let mut fields = Vec::new();
|
||||
|
||||
if let Some(temp) = data.get("cpu_temp_celsius").and_then(|t| t.as_f64()) {
|
||||
fields.push(("CPU Temp", format!("{:.1}°C", temp), true));
|
||||
}
|
||||
if let Some(load) = data.get("load_average").and_then(|l| l.as_array()) {
|
||||
let loads: Vec<String> = load.iter().filter_map(|v| v.as_f64()).map(|v| format!("{:.2}", v)).collect();
|
||||
fields.push(("Load Average", loads.join(", "), true));
|
||||
}
|
||||
if let Some(total) = data.get("memory_total_kb").and_then(|t| t.as_u64()) {
|
||||
if let Some(avail) = data.get("memory_available_kb").and_then(|a| a.as_u64()) {
|
||||
let used = total.saturating_sub(avail);
|
||||
fields.push(("Memory", format!("{} / {} MB", used / 1024, total / 1024), true));
|
||||
}
|
||||
}
|
||||
if let Some(total) = data.get("disk_total_bytes").and_then(|t| t.as_u64()) {
|
||||
if let Some(avail) = data.get("disk_available_bytes").and_then(|a| a.as_u64()) {
|
||||
let used = total.saturating_sub(avail);
|
||||
fields.push(("Disk", format!("{:.1} / {:.1} GB", used as f64 / 1e9, total as f64 / 1e9), true));
|
||||
}
|
||||
}
|
||||
if let Some(uptime) = data.get("uptime_secs").and_then(|u| u.as_u64()) {
|
||||
let hours = uptime / 3600;
|
||||
let mins = (uptime % 3600) / 60;
|
||||
fields.push(("Uptime", format!("{}h {}m", hours, mins), true));
|
||||
}
|
||||
if let Some(version) = data.get("nixos_version").and_then(|v| v.as_str()) {
|
||||
fields.push(("NixOS", version.to_string(), true));
|
||||
}
|
||||
if let Some(commit) = data.get("commit").and_then(|c| c.as_str()) {
|
||||
fields.push(("Commit", commit.to_string(), true));
|
||||
}
|
||||
|
||||
CreateEmbed::new()
|
||||
.title("Noisebridge Pi Info")
|
||||
.colour(Colour::BLUE)
|
||||
.fields(fields)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to parse info response");
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to parse Pi info.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to reach the cache service.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
};
|
||||
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new().embed(embed)
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_history(state: &AppState, _command: &CommandInteraction) -> CreateInteractionResponse {
|
||||
let url = format!("{}/history", state.cache_url);
|
||||
let resp = state.client.get(&url).send().await;
|
||||
|
||||
let embed = match resp {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<Vec<serde_json::Value>>().await {
|
||||
Ok(entries) => {
|
||||
let lines: Vec<String> = entries.iter().take(10).map(|entry| {
|
||||
let status = entry.get("status").and_then(|s| s.as_str()).unwrap_or("unknown");
|
||||
let ts = entry.get("timestamp").and_then(|t| t.as_u64()).unwrap_or(0);
|
||||
let emoji = match status {
|
||||
"open" => "🟢",
|
||||
"closed" => "🔴",
|
||||
"offline" => "⚪",
|
||||
_ => "❓",
|
||||
};
|
||||
format!("{} **{}** — {}", emoji, status, format_timestamp(ts))
|
||||
}).collect();
|
||||
|
||||
let description = if lines.is_empty() {
|
||||
"No history available.".to_string()
|
||||
} else {
|
||||
lines.join("\n")
|
||||
};
|
||||
|
||||
CreateEmbed::new()
|
||||
.title("Recent Door History")
|
||||
.description(description)
|
||||
.colour(Colour::BLUE)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to parse history response");
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to parse history.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
CreateEmbed::new()
|
||||
.title("Error")
|
||||
.description("Failed to reach the cache service.")
|
||||
.colour(Colour::from_rgb(255, 0, 0))
|
||||
}
|
||||
};
|
||||
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new().embed(embed)
|
||||
)
|
||||
CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().embed(embed))
|
||||
}
|
||||
|
||||
struct Handler {
|
||||
|
|
@ -256,11 +150,7 @@ 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"),
|
||||
CreateCommand::new("info").description("Show Pi system information"),
|
||||
CreateCommand::new("history").description("Show recent door history"),
|
||||
];
|
||||
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");
|
||||
|
|
@ -273,13 +163,9 @@ impl serenity::all::EventHandler for Handler {
|
|||
if let Interaction::Command(command) = interaction {
|
||||
let response = match command.data.name.as_str() {
|
||||
"status" => handle_status(&self.state, &command).await,
|
||||
"info" => handle_info(&self.state, &command).await,
|
||||
"history" => handle_history(&self.state, &command).await,
|
||||
_ => {
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new().content("Unknown command.")
|
||||
)
|
||||
}
|
||||
_ => CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new().content("Unknown command."),
|
||||
),
|
||||
};
|
||||
|
||||
if let Err(e) = command.create_response(&ctx.http, response).await {
|
||||
|
|
@ -295,8 +181,8 @@ async fn main() -> Result<()> {
|
|||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let discord_token = std::env::var("NOISEBELL_DISCORD_TOKEN")
|
||||
.context("NOISEBELL_DISCORD_TOKEN is required")?;
|
||||
let discord_token =
|
||||
std::env::var("NOISEBELL_DISCORD_TOKEN").context("NOISEBELL_DISCORD_TOKEN is required")?;
|
||||
|
||||
let channel_id: u64 = std::env::var("NOISEBELL_DISCORD_CHANNEL_ID")
|
||||
.context("NOISEBELL_DISCORD_CHANNEL_ID is required")?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue