feat: add remote, with rss, cache, discord, and zulip
This commit is contained in:
parent
50ec63a474
commit
68c4f8a1fc
40 changed files with 10455 additions and 40 deletions
1614
remote/matrix-bot/Cargo.lock
generated
Normal file
1614
remote/matrix-bot/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
remote/matrix-bot/Cargo.toml
Normal file
14
remote/matrix-bot/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "noisebell-matrix"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
45
remote/matrix-bot/flake.nix
Normal file
45
remote/matrix-bot/flake.nix
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
description = "Noisebell - Matrix 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-matrix = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages.${system}.default = noisebell-matrix;
|
||||
|
||||
devShells.${system}.default = craneLib.devShell {
|
||||
packages = [ pkgs.rust-analyzer ];
|
||||
};
|
||||
};
|
||||
}
|
||||
176
remote/matrix-bot/src/main.rs
Normal file
176
remote/matrix-bot/src/main.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
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 tracing::{error, info};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WebhookPayload {
|
||||
status: String,
|
||||
#[allow(dead_code)]
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
client: reqwest::Client,
|
||||
webhook_secret: String,
|
||||
homeserver: String,
|
||||
access_token: String,
|
||||
room_id: String,
|
||||
txn_counter: AtomicU64,
|
||||
}
|
||||
|
||||
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 percent_encode_room_id(room_id: &str) -> String {
|
||||
room_id
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'!' => "%21".to_string(),
|
||||
':' => "%3A".to_string(),
|
||||
'#' => "%23".to_string(),
|
||||
_ => c.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_message(status: &str) -> (&str, &str) {
|
||||
match status {
|
||||
"open" => (
|
||||
"Door is open at Noisebridge.",
|
||||
"<b>Door is open</b> at Noisebridge.",
|
||||
),
|
||||
"closed" => (
|
||||
"Door is closed at Noisebridge.",
|
||||
"<b>Door is closed</b> at Noisebridge.",
|
||||
),
|
||||
"offline" => (
|
||||
"Pi is offline — the Noisebridge Pi is unreachable.",
|
||||
"<b>Pi is offline</b> — the Noisebridge Pi is unreachable.",
|
||||
),
|
||||
_ => ("Unknown status.", "Unknown status."),
|
||||
}
|
||||
}
|
||||
|
||||
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, "received webhook");
|
||||
|
||||
let (plain, html) = format_message(&body.status);
|
||||
let txn_id = state.txn_counter.fetch_add(1, Ordering::Relaxed);
|
||||
let encoded_room = percent_encode_room_id(&state.room_id);
|
||||
let url = format!(
|
||||
"{}/_matrix/client/v3/rooms/{}/send/m.room.message/noisebell-{}",
|
||||
state.homeserver, encoded_room, txn_id
|
||||
);
|
||||
|
||||
let result = state
|
||||
.client
|
||||
.put(&url)
|
||||
.bearer_auth(&state.access_token)
|
||||
.json(&serde_json::json!({
|
||||
"msgtype": "m.notice",
|
||||
"body": plain,
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": html,
|
||||
}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
info!(status = %body.status, "message sent to Matrix");
|
||||
StatusCode::OK
|
||||
}
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
error!(%status, %body, "Matrix API error");
|
||||
StatusCode::BAD_GATEWAY
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to contact Matrix homeserver");
|
||||
StatusCode::BAD_GATEWAY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let port: u16 = std::env::var("NOISEBELL_MATRIX_PORT")
|
||||
.unwrap_or_else(|_| "3004".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_MATRIX_PORT must be a valid u16")?;
|
||||
|
||||
let webhook_secret = std::env::var("NOISEBELL_MATRIX_WEBHOOK_SECRET")
|
||||
.context("NOISEBELL_MATRIX_WEBHOOK_SECRET is required")?;
|
||||
|
||||
let homeserver = std::env::var("NOISEBELL_MATRIX_HOMESERVER")
|
||||
.context("NOISEBELL_MATRIX_HOMESERVER is required (e.g. https://matrix.org)")?;
|
||||
|
||||
let access_token = std::env::var("NOISEBELL_MATRIX_ACCESS_TOKEN")
|
||||
.context("NOISEBELL_MATRIX_ACCESS_TOKEN is required")?;
|
||||
|
||||
let room_id = std::env::var("NOISEBELL_MATRIX_ROOM_ID")
|
||||
.context("NOISEBELL_MATRIX_ROOM_ID is required (e.g. !abc123:matrix.org)")?;
|
||||
|
||||
info!(port, %homeserver, %room_id, "starting noisebell-matrix");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.context("failed to build HTTP client")?;
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
client,
|
||||
webhook_secret,
|
||||
homeserver,
|
||||
access_token,
|
||||
room_id,
|
||||
txn_counter: AtomicU64::new(0),
|
||||
});
|
||||
|
||||
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, "listening");
|
||||
|
||||
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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue