Mashallah we got telegram now :o
This commit is contained in:
1 parent
b6094834bb
commit
581ecc8590
5 files changed
+46
-9
No files matched your search
+8
-4
@@ -4,8 +4,12 @@ services:
|
|||||||
container_name: labdeck
|
container_name: labdeck
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
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:
|
volumes:
|
||||||
- ./data:/data
|
- labdeck-data:/data # named volume: Docker sets the right owner automatically
|
||||||
# Health checks of LAN hosts work with the default bridge network.
|
# Bind mount instead? Create the folder first and give it to the container user:
|
||||||
# If you need to reach services bound to the Docker host itself, use network_mode: host instead.
|
# mkdir -p data && sudo chown 10001:10001 data
|
||||||
|
# - ./data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
labdeck-data:
|
||||||
+19
-1
@@ -34,6 +34,21 @@ pub async fn send(app: &App, title: &str, body: &str, good: bool) -> Result<(),
|
|||||||
"slack" => client
|
"slack" => client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.json(&json!({ "text": format!("{} *{}*\n{}", if good { ":large_green_circle:" } else { ":red_circle:" }, title, body) })),
|
.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" => {
|
"ntfy" => {
|
||||||
let mut r = client
|
let mut r = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
@@ -63,6 +78,9 @@ pub async fn send(app: &App, title: &str, body: &str, good: bool) -> Result<(),
|
|||||||
if resp.status().is_success() {
|
if resp.status().is_success() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} 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
@@ -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://")) {
|
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://"));
|
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"));
|
return Err(bad("unknown webhook type"));
|
||||||
}
|
}
|
||||||
let c = app.db.lock().unwrap();
|
let c = app.db.lock().unwrap();
|
||||||
|
|||||||
+6
-1
@@ -53,7 +53,12 @@ async fn main() {
|
|||||||
let _ = std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700));
|
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 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 {
|
let app = Arc::new(App {
|
||||||
db: Mutex::new(conn),
|
db: Mutex::new(conn),
|
||||||
|
|||||||
+12
-2
@@ -850,9 +850,18 @@ async function buildSettings() {
|
|||||||
class: 'btn' + (currentTheme() === id ? ' primary' : ''), on: { click: () => { applyTheme(id); buildSettings(); } },
|
class: 'btn' + (currentTheme() === id ? ' primary' : ''), on: { click: () => { applyTheme(id); buildSettings(); } },
|
||||||
}, h('span', { class: 'sw', style: 'background:' + c }), id)));
|
}, 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)));
|
.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 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' },
|
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('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('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)),
|
h('div', { class: 'row' }, h('div', { style: 'width:180px' }, kind), h('div', { class: 'grow' }, url)),
|
||||||
|
hookHelp,
|
||||||
h('div', { class: 'row' },
|
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 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'))),
|
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'))),
|
||||||
|
|||||||
Reference in new issue
Block a user