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
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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'))),
|
||||
|
||||
Reference in new issue
Block a user