initial shit
This commit is contained in:
commit
1cbc553e0b
|
@ -0,0 +1,4 @@
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
config.json
|
|
@ -0,0 +1,172 @@
|
||||||
|
{
|
||||||
|
"name": "uptimenotify",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "uptimenotify",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "LGPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"@network-utils/tcp-ping": "^1.2.3",
|
||||||
|
"discord-webhook-node": "^1.1.8",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@network-utils/tcp-ping": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@network-utils/tcp-ping/-/tcp-ping-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-YKSnfvKGabggw+r6xVk9mxF5AdvDSL0pXX2EydkaBpIspcOokK7HsG7N+i94eaakDnlVFxN2EcXEh0UYOSUKvw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.10.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
|
||||||
|
"integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/discord-webhook-node": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-webhook-node/-/discord-webhook-node-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-3u0rrwywwYGc6HrgYirN/9gkBYqmdpvReyQjapoXARAHi0P0fIyf3W5tS5i3U3cc7e44E+e7dIHYUeec7yWaug==",
|
||||||
|
"dependencies": {
|
||||||
|
"form-data": "^3.0.0",
|
||||||
|
"node-fetch": "^2.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||||
|
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "uptimenotify",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node .",
|
||||||
|
"dev": "tsc && node ."
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "LGPL-3.0-only",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@network-utils/tcp-ping": "^1.2.3",
|
||||||
|
"discord-webhook-node": "^1.1.8",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { readFileSync } from "fs"
|
||||||
|
|
||||||
|
export const defaultInterval = 60
|
||||||
|
export const defaultNotifyInterval = 300
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
alertEndpoints: ({
|
||||||
|
enabled: boolean
|
||||||
|
name: string
|
||||||
|
} & ({
|
||||||
|
type: "discord-webhook"
|
||||||
|
webhookUrl: string
|
||||||
|
} | {
|
||||||
|
type: "telegram"
|
||||||
|
token: string
|
||||||
|
chatIds: string[]
|
||||||
|
} | {
|
||||||
|
type: "pushover"
|
||||||
|
token: string
|
||||||
|
destinationIds: string[]
|
||||||
|
}))[],
|
||||||
|
pollEndpoints: {
|
||||||
|
name: string
|
||||||
|
endpoint: string
|
||||||
|
interval?: number
|
||||||
|
notifyInterval?: number
|
||||||
|
type: "fetch" | "ping"
|
||||||
|
alertEndpoints: string[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = JSON.parse(readFileSync("./config.json").toString()) as Config
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { State } from "."
|
||||||
|
import { Config, defaultNotifyInterval } from "./config"
|
||||||
|
import { notify } from "./notify"
|
||||||
|
import { formatTS } from "./utils"
|
||||||
|
|
||||||
|
type EndpointState = State extends Map<any, infer L> ? L : never
|
||||||
|
|
||||||
|
export const handleDown = (endpointState: EndpointState, curTime: number, endpoint: Config["pollEndpoints"][number]) => {
|
||||||
|
const prevDown = endpointState.isDown
|
||||||
|
endpointState.isDown = true
|
||||||
|
if (!prevDown) endpointState.downStart = curTime
|
||||||
|
|
||||||
|
if (curTime - endpointState.lastDownAlert < (endpoint.notifyInterval ?? defaultNotifyInterval) * 1000) return
|
||||||
|
const message = prevDown ?
|
||||||
|
`[${formatTS(curTime)}] ${endpoint.name} is still down and initially went down at ${formatTS(endpointState.downStart)}` :
|
||||||
|
`[${formatTS(curTime)}] ${endpoint.name} went down`
|
||||||
|
|
||||||
|
notify(message, endpoint)
|
||||||
|
endpointState.lastDownAlert = curTime
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleUp = (endpointState: EndpointState, curTime: number, endpoint: Config["pollEndpoints"][number]) => {
|
||||||
|
if (endpointState.isDown) {
|
||||||
|
endpointState.isDown = false
|
||||||
|
const message = `[${formatTS(curTime)}] ${endpoint.name} is back up`
|
||||||
|
notify(message, endpoint)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { ping } from "@network-utils/tcp-ping"
|
||||||
|
import { defaultInterval, config } from "./config"
|
||||||
|
import { sleep } from "./utils"
|
||||||
|
import { handleDown, handleUp } from "./handler"
|
||||||
|
|
||||||
|
let state = new Map(config.pollEndpoints.map(x => [x.name, {
|
||||||
|
lastExec: 0,
|
||||||
|
downStart: 0,
|
||||||
|
lastDownAlert: 0,
|
||||||
|
isDown: false
|
||||||
|
}]))
|
||||||
|
export type State = typeof state
|
||||||
|
|
||||||
|
const executor = async () => {
|
||||||
|
const curTime = Date.now()
|
||||||
|
for (const endpoint of config.pollEndpoints) {
|
||||||
|
const endpointState = state.get(endpoint.name)
|
||||||
|
if (endpointState === undefined) {console.log(`Could not find endpoint for ${endpoint.name}`)}
|
||||||
|
else if (curTime - endpointState.lastExec > ((endpoint.interval ?? defaultInterval) * 1000)) {
|
||||||
|
if (endpoint.type === "fetch" ) {
|
||||||
|
const r = await fetch(endpoint.endpoint, {method: "GET"})
|
||||||
|
if (r.ok) handleUp(endpointState, curTime, endpoint)
|
||||||
|
else handleDown(endpointState, curTime, endpoint)
|
||||||
|
}
|
||||||
|
else if (endpoint.type === "ping" ) {
|
||||||
|
const r = await ping({address: endpoint.endpoint})
|
||||||
|
if (r.errors.length === 0) handleUp(endpointState, curTime, endpoint)
|
||||||
|
else handleDown(endpointState, curTime, endpoint)
|
||||||
|
}
|
||||||
|
console.log(`Time to poll ${endpoint.name}`)
|
||||||
|
endpointState.lastExec = curTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
while (true) {
|
||||||
|
await sleep(1000)
|
||||||
|
executor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const sendDiscord = async (webhookUrl: string, message: string) => {
|
||||||
|
const pDiscordResult = await fetch(webhookUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return pDiscordResult.text()
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { config, Config } from "../config"
|
||||||
|
import { sendTelegram } from "./telegram"
|
||||||
|
import { sendDiscord } from "./discord"
|
||||||
|
import { sendPushover } from "./pushover"
|
||||||
|
|
||||||
|
const alertEndpointMap = new Map(config.alertEndpoints.map(({name, ...x}) => [name, x]))
|
||||||
|
|
||||||
|
export const notify = (message: string, endpoint: Config["pollEndpoints"][number]) => {
|
||||||
|
for (const dest of endpoint.alertEndpoints) {
|
||||||
|
const d = alertEndpointMap.get(dest)
|
||||||
|
if (d === undefined)
|
||||||
|
console.log(`Could not find any valid alert destination ${dest}`)
|
||||||
|
else if (d.type === "discord-webhook")
|
||||||
|
sendDiscord(d.webhookUrl, message)
|
||||||
|
else if (d.type === "telegram")
|
||||||
|
for (const chatId of d.chatIds) sendTelegram(chatId, d.token, message)
|
||||||
|
else if (d.type === "pushover")
|
||||||
|
for (const de of d.destinationIds) sendPushover(de, d.token, message)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const sendPushover = async (destId: string, token: string, message: string, priority: number = 0, url?: string) => {
|
||||||
|
const psendResult = await fetch("https://api.pushover.net/1/messages.json", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: token,
|
||||||
|
user: destId,
|
||||||
|
message,
|
||||||
|
priority,
|
||||||
|
url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return psendResult.json() as Promise<{status: number, request: string}>
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const sendTelegram = async (cID: string, token: string, message: string) => {
|
||||||
|
const pTelegramResult = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: message,
|
||||||
|
chat_id: cID
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return pTelegramResult.json()
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const sleep = async (ms: number) => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const zeroPad = (numbskull: number, length: number = 2) => numbskull.toString().padStart(length,"0")
|
||||||
|
|
||||||
|
export const formatTS = (timestamp: number) => {
|
||||||
|
const d = new Date(timestamp)
|
||||||
|
return `${d.getFullYear()}-${zeroPad(d.getMonth())}-${zeroPad(d.getDate())} ${zeroPad(d.getHours())}:${zeroPad(d.getMinutes())}:${zeroPad(d.getSeconds())}.${zeroPad(d.getMilliseconds(), 3)}`
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"target": "ES2022",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue