feat: add remote, with rss, cache, discord, and zulip
This commit is contained in:
parent
50ec63a474
commit
83baab68e0
32 changed files with 6615 additions and 40 deletions
2092
remote/discord-bot/Cargo.lock
generated
Normal file
2092
remote/discord-bot/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
remote/discord-bot/Cargo.toml
Normal file
14
remote/discord-bot/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "noisebell-discord"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "model", "rustls_backend"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
64
remote/discord-bot/flake.lock
generated
Normal file
64
remote/discord-bot/flake.lock
generated
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1773115265,
|
||||
"narHash": "sha256-5fDkKTYEgue2klksd52WvcXfZdY1EIlbk0QggAwpFog=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "27711550d109bf6236478dc9f53b9e29c1a374c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772963539,
|
||||
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773115373,
|
||||
"narHash": "sha256-bfK9FJFcQth6f3ydYggS5m0z2NRGF/PY6Y2XgZDJ6pg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "1924b4672a2b8e4aee6e6652ec2e59a8d3c5648e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
45
remote/discord-bot/flake.nix
Normal file
45
remote/discord-bot/flake.nix
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
description = "Noisebell - Discord bot";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, rust-overlay }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default;
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
doCheck = false;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
noisebell-discord = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages.${system}.default = noisebell-discord;
|
||||
|
||||
devShells.${system}.default = craneLib.devShell {
|
||||
packages = [ pkgs.rust-analyzer ];
|
||||
};
|
||||
};
|
||||
}
|
||||
1
remote/discord-bot/result
Symbolic link
1
remote/discord-bot/result
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/nix/store/jnkl079ljh32p7m9jxnan8wis1sp2bm9-noisebell-discord-0.1.0
|
||||
142
remote/discord-bot/src/main.rs
Normal file
142
remote/discord-bot/src/main.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::extract::State;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::post;
|
||||
use axum::{Json, Router};
|
||||
use serde::Deserialize;
|
||||
use serenity::all::{ChannelId, Colour, CreateEmbed, CreateMessage, GatewayIntents, Http};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WebhookPayload {
|
||||
status: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
http: Arc<Http>,
|
||||
channel_id: ChannelId,
|
||||
webhook_secret: String,
|
||||
}
|
||||
|
||||
fn validate_bearer(headers: &HeaderMap, expected: &str) -> bool {
|
||||
headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.strip_prefix("Bearer ").unwrap_or("") == expected)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
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 client = serenity::Client::builder(&discord_token, intents)
|
||||
.event_handler(Handler)
|
||||
.await
|
||||
.context("failed to create Discord client")?;
|
||||
|
||||
let http = client.http.clone();
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
http,
|
||||
channel_id: ChannelId::new(channel_id),
|
||||
webhook_secret,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/webhook", post(post_webhook))
|
||||
.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");
|
||||
|
||||
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.context("failed to register SIGTERM handler")?;
|
||||
|
||||
tokio::select! {
|
||||
result = client.start() => {
|
||||
if let Err(e) = result {
|
||||
error!(error = %e, "Discord client error");
|
||||
}
|
||||
}
|
||||
result = axum::serve(listener, app).with_graceful_shutdown(async move { sigterm.recv().await; }) => {
|
||||
if let Err(e) = result {
|
||||
error!(error = %e, "HTTP server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("shutdown complete");
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue