154 lines
5.2 KiB
Rust
154 lines
5.2 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context, Result};
|
|
use axum::extract::State;
|
|
use axum::http::{HeaderMap, StatusCode};
|
|
use axum::routing::{get, post};
|
|
use axum::{Json, Router};
|
|
use noisebell_common::{validate_bearer, WebhookPayload};
|
|
use serenity::all::{ChannelId, Colour, CreateEmbed, CreateMessage, GatewayIntents};
|
|
use tower_http::trace::TraceLayer;
|
|
use tracing::{error, info, Level};
|
|
|
|
struct AppState {
|
|
http: Arc<serenity::all::Http>,
|
|
channel_id: ChannelId,
|
|
webhook_secret: String,
|
|
}
|
|
|
|
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."),
|
|
};
|
|
|
|
CreateEmbed::new()
|
|
.title(title)
|
|
.description(description)
|
|
.colour(colour)
|
|
.timestamp(serenity::model::Timestamp::from_unix_timestamp(timestamp as i64).unwrap_or_else(|_| serenity::model::Timestamp::now()))
|
|
}
|
|
|
|
async fn post_webhook(
|
|
State(state): State<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
Json(body): Json<WebhookPayload>,
|
|
) -> StatusCode {
|
|
if !validate_bearer(&headers, &state.webhook_secret) {
|
|
return StatusCode::UNAUTHORIZED;
|
|
}
|
|
|
|
info!(status = %body.status, timestamp = body.timestamp, "received webhook");
|
|
|
|
let embed = build_embed(&body.status, body.timestamp);
|
|
let message = CreateMessage::new().embed(embed);
|
|
|
|
match state.channel_id.send_message(&state.http, message).await {
|
|
Ok(_) => {
|
|
info!(status = %body.status, "embed sent to Discord");
|
|
StatusCode::OK
|
|
}
|
|
Err(e) => {
|
|
error!(error = %e, "failed to send embed to Discord");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Handler;
|
|
|
|
impl serenity::all::EventHandler for Handler {}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
tracing_subscriber::fmt()
|
|
.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 channel_id: u64 = std::env::var("NOISEBELL_DISCORD_CHANNEL_ID")
|
|
.context("NOISEBELL_DISCORD_CHANNEL_ID is required")?
|
|
.parse()
|
|
.context("NOISEBELL_DISCORD_CHANNEL_ID must be a valid u64")?;
|
|
|
|
let webhook_secret = std::env::var("NOISEBELL_DISCORD_WEBHOOK_SECRET")
|
|
.context("NOISEBELL_DISCORD_WEBHOOK_SECRET is required")?;
|
|
|
|
let port: u16 = std::env::var("NOISEBELL_DISCORD_PORT")
|
|
.unwrap_or_else(|_| "3001".into())
|
|
.parse()
|
|
.context("NOISEBELL_DISCORD_PORT must be a valid u16")?;
|
|
|
|
info!(port, channel_id, "starting noisebell-discord");
|
|
|
|
let intents = GatewayIntents::empty();
|
|
let mut initial_client = serenity::Client::builder(&discord_token, intents)
|
|
.event_handler(Handler)
|
|
.await
|
|
.context("failed to create Discord client")?;
|
|
|
|
let http = initial_client.http.clone();
|
|
|
|
let app_state = Arc::new(AppState {
|
|
http,
|
|
channel_id: ChannelId::new(channel_id),
|
|
webhook_secret,
|
|
});
|
|
|
|
let app = Router::new()
|
|
.route("/health", get(|| async { StatusCode::OK }))
|
|
.route("/webhook", post(post_webhook))
|
|
.layer(
|
|
TraceLayer::new_for_http()
|
|
.make_span_with(tower_http::trace::DefaultMakeSpan::new().level(Level::INFO))
|
|
.on_response(tower_http::trace::DefaultOnResponse::new().level(Level::INFO)),
|
|
)
|
|
.with_state(app_state);
|
|
|
|
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
|
|
.await
|
|
.context(format!("failed to bind to 0.0.0.0:{port}"))?;
|
|
|
|
info!(port, "webhook listener ready");
|
|
|
|
// Gateway reconnect loop — the Http client for sending messages is independent
|
|
let token_for_gateway = discord_token.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(e) = initial_client.start().await {
|
|
error!(error = %e, "Discord gateway disconnected");
|
|
}
|
|
loop {
|
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
info!("reconnecting to Discord gateway");
|
|
match serenity::Client::builder(&token_for_gateway, GatewayIntents::empty())
|
|
.event_handler(Handler)
|
|
.await
|
|
{
|
|
Ok(mut client) => {
|
|
if let Err(e) = client.start().await {
|
|
error!(error = %e, "Discord gateway disconnected");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!(error = %e, "failed to create Discord client");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
|
.context("failed to register SIGTERM handler")?;
|
|
axum::serve(listener, app)
|
|
.with_graceful_shutdown(async move {
|
|
sigterm.recv().await;
|
|
})
|
|
.await
|
|
.context("server error")?;
|
|
|
|
info!("shutdown complete");
|
|
Ok(())
|
|
}
|