CCCPaste

uptimekuma.tcl

########## CONFIG ##########

set kuma_webhook_port   8787
set kuma_webhook_path   "/kuma"
set kuma_webhook_chan   "#status"

# Optional shared secret (must match X-Kuma-Secret header in Kuma)
set kuma_webhook_secret "yourSecretHere"

########## INTERNAL STATE – DO NOT EDIT ##########
array set kuma_state       {}
array set kuma_method      {}
array set kuma_path        {}
array set kuma_len         {}
array set kuma_body        {}
array set kuma_secret_hdr  {}

########## COLORS ##########
# IRC color codes
set kuma_color_reset "\017"
set kuma_color_red   "\00304"
set kuma_color_green "\00303"

########## LISTENER ##########

if {![info exists kuma_webhook_initialized]} {
    set kuma_webhook_initialized 1
    listen $kuma_webhook_port script kuma:accept
    putlog "Uptime Kuma webhook: listening on port $kuma_webhook_port, path $kuma_webhook_path"
}

proc kuma:accept {idx} {
    global kuma_state kuma_len kuma_body kuma_secret_hdr
    set kuma_state($idx) "request"
    set kuma_len($idx) 0
    set kuma_body($idx) ""
    set kuma_secret_hdr($idx) ""
    control $idx kuma:control
    return 0
}

proc kuma:control {idx text} {
    global kuma_state kuma_method kuma_path kuma_len kuma_body kuma_secret_hdr

    if {![info exists kuma_state($idx)]} {
        set kuma_state($idx) "request"
    }

    switch -- $kuma_state($idx) {
        request {
            if {![regexp {^([A-Z]+) ([^ ]+) HTTP/} $text -> method path]} {
                kuma:respond $idx 400 "Bad Request"
                return 0
            }
            set kuma_method($idx) $method
            set kuma_path($idx)   $path
            set kuma_state($idx)  "headers"
        }

        headers {
            if {$text eq ""} {
                set kuma_state($idx) "body"
                if {$kuma_len($idx) == 0} {
                    kuma:handle $idx
                }
                return 0
            }
            if {[regexp -nocase {^Content-Length:\s*([0-9]+)} $text -> len]} {
                set kuma_len($idx) $len
            } elseif {[regexp -nocase {^X-Kuma-Secret:\s*(.+)} $text -> sec]} {
                set kuma_secret_hdr($idx) [string trim $sec]
            }
        }

        body {
            append kuma_body($idx) $text
            if {$kuma_len($idx) == 0 || [string length $kuma_body($idx)] >= $kuma_len($idx)} {
                kuma:handle $idx
            }
        }
    }

    return 0
}

########## REQUEST HANDLING ##########

proc kuma:handle {idx} {
    global kuma_method kuma_path kuma_body kuma_secret_hdr
    global kuma_webhook_path kuma_webhook_secret kuma_webhook_chan
    global kuma_color_reset kuma_color_red kuma_color_green

    if {$kuma_method($idx) ne "POST"} {
        kuma:respond $idx 405 "Method Not Allowed"
        return
    }

    set fullPath $kuma_path($idx)
    if {[regexp {^([^?]+)} $fullPath -> cleanPath]} {
        set pathOnly $cleanPath
    } else {
        set pathOnly $fullPath
    }

    if {$pathOnly ne $kuma_webhook_path} {
        kuma:respond $idx 404 "Not Found"
        return
    }

    if {$kuma_webhook_secret ne ""} {
        if {![info exists kuma_secret_hdr($idx)] || $kuma_secret_hdr($idx) ne $kuma_webhook_secret} {
            kuma:respond $idx 403 "Forbidden"
            return
        }
    }

    set body ""
    if {[info exists kuma_body($idx)]} {
        set body [string trim $kuma_body($idx)]
    }

    # --- Simple JSON-ish parsing for: {"monitor":"...","status":"...","reason":"..."} ---
    set monitor ""
    set status  ""
    set reason  ""

    if {[regexp {"monitor"\s*:\s*"([^"]*)"} $body -> m]} {
        set monitor [string trim $m]
    }
    if {[regexp {"status"\s*:\s*"([^"]*)"} $body -> s]} {
        set status [string toupper [string trim $s]]
    }
    if {[regexp {"reason"\s*:\s*"([^"]*)"} $body -> r]} {
        # Unescape basic things
        set reason [string map {\\n " " \\r " " \\" "\""} $r]
        set reason [string trim $reason]
    }

    if {$monitor eq ""} { set monitor "Unknown Monitor" }
    if {$reason eq ""}  { set reason "No reason provided" }

    # Build colored status text
    if {$status eq "UP"} {
        set status_colored "${kuma_color_green}UP${kuma_color_reset}"
    } elseif {$status eq "DOWN"} {
        set status_colored "${kuma_color_red}DOWN${kuma_color_reset}"
    } else {
        # Fallback if status is weird
        set status_colored $status
    }

    # Line 1: " is (coloured)UP/DOWN"
    set line1 "$monitor is $status_colored"

    # Line 2:
    # Down: " Failed - "
    # Up:   " Up - "
    if {$status eq "DOWN"} {
        set line2 "$monitor Failed - $reason"
    } elseif {$status eq "UP"} {
        set line2 "$monitor Up - $reason"
    } else {
        set line2 "$monitor - $reason"
    }

    if {$kuma_webhook_chan ne ""} {
        putserv "PRIVMSG $kuma_webhook_chan :$line1"
        putserv "PRIVMSG $kuma_webhook_chan :$line2"
    }

    kuma:respond $idx 200 "OK"
}

########## RESPONSE & CLEANUP ##########

proc kuma:respond {idx code text} {
    set body $text
    set len  [string length $body]

    putdcc $idx "HTTP/1.1 $code $text"
    putdcc $idx "Content-Type: text/plain"
    putdcc $idx "Content-Length: $len"
    putdcc $idx ""
    putdcc $idx $body

    kuma:cleanup $idx
}

proc kuma:cleanup {idx} {
    global kuma_state kuma_method kuma_path kuma_len kuma_body kuma_secret_hdr

    catch {unset kuma_state($idx)}
    catch {unset kuma_method($idx)}
    catch {unset kuma_path($idx)}
    catch {unset kuma_len($idx)}
    catch {unset kuma_body($idx)}
    catch {unset kuma_secret_hdr($idx)}

    catch {killdcc $idx}
}