initial shit

This commit is contained in:
Lumeille 2024-03-09 22:27:06 +11:00
commit 1cbc553e0b
12 changed files with 386 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
dist/
node_modules/
.env
config.json

172
package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

22
package.json Normal file
View File

@ -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"
}
}

32
src/config.ts Normal file
View File

@ -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

28
src/handler.ts Normal file
View File

@ -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)
}
}

43
src/index.ts Normal file
View File

@ -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()

12
src/notify/discord.ts Normal file
View File

@ -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()
}

20
src/notify/index.ts Normal file
View File

@ -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)
}
}

16
src/notify/pushover.ts Normal file
View File

@ -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}>
}

13
src/notify/telegram.ts Normal file
View File

@ -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()
}

10
src/utils.ts Normal file
View File

@ -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)}`
}

14
tsconfig.json Normal file
View File

@ -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
}
}