Mashallah we got telegram now :o

This commit is contained in:
nke4 committed 2026-10-27 11:32:37 +01:00
1 parent b6094834bb
commit 581ecc8590
5 files changed
+46 -9

No files matched your search

+8 -4
View File
@@ -4,8 +4,12 @@ services:
container_name: labdeck
restart: unless-stopped
ports:
- "8412:8787" # put a TLS reverse proxy in front (Caddy, Traefik, Pangolin ...), change port to your liking
- "8787:8787" # put a TLS reverse proxy in front (Caddy, Traefik, Pangolin ...), change port to your liking
volumes:
- ./data:/data
# Health checks of LAN hosts work with the default bridge network.
# If you need to reach services bound to the Docker host itself, use network_mode: host instead.
- labdeck-data:/data # named volume: Docker sets the right owner automatically
# Bind mount instead? Create the folder first and give it to the container user:
# mkdir -p data && sudo chown 10001:10001 data
# - ./data:/data
volumes:
labdeck-data:
+19 -1
View File
@@ -34,6 +34,21 @@ pub async fn send(app: &App, title: &str, body: &str, good: bool) -> Result<(),
"slack" => client
.post(&url)
.json(&json!({ "text": format!("{} *{}*\n{}", if good { ":large_green_circle:" } else { ":red_circle:" }, title, body) })),
"telegram" => {
// URL: https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<ID>
let mut u = reqwest::Url::parse(&url).map_err(|e| format!("invalid Telegram URL: {e}"))?;
let chat_id = u
.query_pairs()
.find(|(k, _)| k == "chat_id")
.map(|(_, v)| v.into_owned())
.ok_or("Telegram URL needs ?chat_id=<ID> at the end")?;
u.set_query(None);
client.post(u).json(&json!({
"chat_id": chat_id,
"text": format!("{} {}\n{}", if good { "🟢" } else { "🔴" }, title, body),
"disable_web_page_preview": true,
}))
}
"ntfy" => {
let mut r = client
.post(&url)
@@ -63,6 +78,9 @@ pub async fn send(app: &App, title: &str, body: &str, good: bool) -> Result<(),
if resp.status().is_success() {
Ok(())
} else {
Err(format!("webhook answered HTTP {}", resp.status().as_u16()))
let code = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
let body: String = body.chars().take(200).collect();
Err(format!("webhook answered HTTP {code}: {body}"))
}
}
+1 -1
View File
@@ -504,7 +504,7 @@ async fn settings_put(State(app): Ctx, Json(mut s): Json<Settings>) -> R {
if !s.webhook_url.is_empty() && !(s.webhook_url.starts_with("http://") || s.webhook_url.starts_with("https://")) {
return Err(bad("webhook URL must start with http:// or https://"));
}
if !matches!(s.webhook_kind.as_str(), "json" | "discord" | "slack" | "ntfy") {
if !matches!(s.webhook_kind.as_str(), "json" | "discord" | "telegram" | "slack" | "ntfy") {
return Err(bad("unknown webhook type"));
}
let c = app.db.lock().unwrap();
+6 -1
View File
@@ -53,7 +53,12 @@ async fn main() {
let _ = std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700));
}
let db_path = format!("{}/labdeck.db", data_dir.trim_end_matches('/'));
let conn = db::open(&db_path).expect("cannot open database");
let conn = db::open(&db_path).unwrap_or_else(|e| {
eprintln!("cannot open database {db_path}: {e}");
eprintln!("hint: the data directory must be writable by this process.");
eprintln!(" Docker bind mount: sudo chown 10001:10001 <dir> (or use a named volume)");
std::process::exit(1);
});
let app = Arc::new(App {
db: Mutex::new(conn),
+12 -2
View File
@@ -850,9 +850,18 @@ async function buildSettings() {
class: 'btn' + (currentTheme() === id ? ' primary' : ''), on: { click: () => { applyTheme(id); buildSettings(); } },
}, h('span', { class: 'sw', style: 'background:' + c }), id)));
const kind = h('select', { id: 'wk' }, [['json', 'Generic JSON'], ['discord', 'Discord'], ['slack', 'Slack'], ['ntfy', 'ntfy']]
const kind = h('select', { id: 'wk' }, [['json', 'Generic JSON'], ['discord', 'Discord'], ['telegram', 'Telegram'], ['slack', 'Slack'], ['ntfy', 'ntfy']]
.map(([v, l]) => h('option', { value: v, selected: st.webhook_kind === v }, l)));
const url = h('input', { type: 'text', id: 'wu', value: st.webhook_url, placeholder: 'https://ntfy.example.com/homelab', spellcheck: false });
const HOOK_HELP = {
json: ['https://example.com/hook', 'POSTs {title, message, status, time, source} as JSON.'],
discord: ['https://discord.com/api/webhooks/<id>/<token>', 'Discord: channel settings > Integrations > Webhooks > New Webhook > Copy Webhook URL.'],
telegram: ['https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<CHAT_ID>', 'Telegram: create a bot with @BotFather for the token, send the bot a message, then find your chat id via https://api.telegram.org/bot<TOKEN>/getUpdates (result > message > chat > id). Groups have negative ids.'],
slack: ['https://hooks.slack.com/services/T000/B000/XXXX', 'Slack: create an Incoming Webhook app and copy its URL.'],
ntfy: ['https://ntfy.sh/my-homelab-alerts', 'ntfy: the full topic URL.'],
};
const url = h('input', { type: 'text', id: 'wu', value: st.webhook_url, placeholder: HOOK_HELP[st.webhook_kind]?.[0] || '', spellcheck: false });
const hookHelp = h('p', { class: 'dim' }, HOOK_HELP[st.webhook_kind]?.[1] || '');
kind.addEventListener('change', () => { url.placeholder = HOOK_HELP[kind.value][0]; hookHelp.textContent = HOOK_HELP[kind.value][1]; });
const saveHook = async () => { await api('PUT', '/api/settings', { webhook_url: url.value, webhook_kind: kind.value }); };
const snipList = h('div', null, S.snippets.length ? S.snippets.map(sn => h('div', { class: 'row' },
@@ -881,6 +890,7 @@ async function buildSettings() {
h('section', { class: 'section' }, h('h2', null, 'Alerts'),
h('p', null, 'Get a message when a service goes down or recovers, or when a certificate is about to expire.'),
h('div', { class: 'row' }, h('div', { style: 'width:180px' }, kind), h('div', { class: 'grow' }, url)),
hookHelp,
h('div', { class: 'row' },
h('button', { class: 'btn primary', on: { click: async () => { try { await saveHook(); toast('Alert settings saved.'); } catch (e) { fail(e); } } } }, 'Save'),
h('button', { class: 'btn', on: { click: async () => { try { await saveHook(); await api('POST', '/api/settings/test-webhook'); toast('Test message sent.'); } catch (e) { fail(e); } } } }, 'Send test message'))),