From 61c1bbfbddb267941e8d07ccacb59d38be7f30f1 Mon Sep 17 00:00:00 2001 From: Reed Date: Sat, 26 Feb 2022 21:54:23 -0500 Subject: [PATCH 1/3] Add game active checks. (#4) This commit adds the ability for HypeTrack to check if a game is active in the API. --- README.md | 1 + src/checks/game.check.ts | 85 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 4 ++ src/utils/discord.ts | 2 +- src/utils/telegram.ts | 2 +- src/utils/twitter.ts | 2 +- 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/checks/game.check.ts diff --git a/README.md b/README.md index 1cec4ac..5085921 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Tracker is the main codebase for [@HQMonitor](//twitter.com/HQMonitor) (also kno Tracker can: * Determine when the server is being re-deployed or scaled up * Determine when an HQ stream has gone live +* Determine when a game is live Tracker also has the ability to send notifications when it detects changes. Currently, it can notify via the following: * Discord webhook (set `DISCORD_WEBHOOK_ID` and `DISCORD_WEBHOOK_SECRET` in your `.env`) diff --git a/src/checks/game.check.ts b/src/checks/game.check.ts new file mode 100644 index 0000000..4380b16 --- /dev/null +++ b/src/checks/game.check.ts @@ -0,0 +1,85 @@ +import axios from 'axios' +import debug from '../utils/debug.js' +import { tweet } from '../utils/twitter.js' +import { client } from '../utils/discord.js' +import { tg } from '../utils/telegram.js' +import { get, set } from '../utils/db2.js' + +import type { HTCheckConfig } from '../types/HTCheckConfig.type.js' + +const key = 'gameLive' + +const config: HTCheckConfig = { + sendToDiscord: true, + sendToTelegram: true, + sendToTwitter: true +} + +const messages = { + gameLive: `An HQ game is active. (ts: ${+new Date()})`, + gameOver: `HQ is no longer active. (ts: ${+new Date()})` +} + +async function social (gameLive: boolean) { + let text = gameLive ? messages.gameLive : messages.gameOver + + if (config.sendToDiscord) { + await client.send(text) + } + + if (config.sendToTwitter) { + await tweet(text) + } + + if (config.sendToTelegram) { + await tg(text) + } +} + +async function check () { + const d = debug.extend('game') + + try { + d('Checking for live game...') + + // Check DB2 for if the game is already live. + const gameAlreadyLive = await get(key) + + if (typeof gameAlreadyLive === 'undefined') { + // Set to false and return. + d(`${key} is not in DB2. Setting to false and leaving until next check.`) + await set(key, false) + return + } + + // Get JSON data from /shows/now. + const { data } = await axios.get('https://api-quiz.hype.space/shows/now') + + if (data.active) { + // If the game is already live, we don't want to spam the notifiers + if (gameAlreadyLive) { + d('Game already marked as live') + return + } + + // If we're here, the game is live and the key for the game already being live is false + d('Game live!') + + // Post on social media + await social(data.active) + } else { + d('Game is not live.') + + // If gameAlreadyLive is true, set key to false and post on social media + if (gameAlreadyLive) { + await set(key, false) + await social(data.active) + } + } + } catch (error: any) { + // oops + d('Game check machine broke. %s', error.message) + } +} + +export default check diff --git a/src/index.ts b/src/index.ts index eb38de7..cba46e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ dotenv.config() import debug from './utils/debug.js' import commonCheck from './checks/common.check.js' import streamCheck from './checks/stream.check.js' +import gameCheck from './checks/game.check.js' import { init, serializeDbToDisk } from './utils/db2.js' import cron from 'node-cron' @@ -31,6 +32,7 @@ process.on('SIGINT', async () => { }) // TODO: Have a way to have this dynamically expandable. +// TODO: Figure out what needs priority here. setInterval(async () => { debug('We\'re on check cycle %d.', cycle) await commonCheck('https://api.prod.hype.space', 'hypeapi') @@ -40,6 +42,8 @@ setInterval(async () => { await streamCheck('internet_high') await streamCheck('intranet_high') await streamCheck('wirecast_high') + + await gameCheck() // Increment cycle debug('Cycle %d ended.', cycle) diff --git a/src/utils/discord.ts b/src/utils/discord.ts index d48846a..70a97c9 100644 --- a/src/utils/discord.ts +++ b/src/utils/discord.ts @@ -11,7 +11,7 @@ const client = new WebhookClient({ }) async function postToDiscord(message: string): Promise { - return await client.send(message) + return client.send(message) } export { diff --git a/src/utils/telegram.ts b/src/utils/telegram.ts index 98cf490..c62c364 100644 --- a/src/utils/telegram.ts +++ b/src/utils/telegram.ts @@ -6,7 +6,7 @@ const client = new Telegram( ) async function tg(message: string): Promise { - return await client.sendMessage( + return client.sendMessage( (process.env.TG_CHAT_ID as string), message ) diff --git a/src/utils/twitter.ts b/src/utils/twitter.ts index f6f1e52..57f12e7 100644 --- a/src/utils/twitter.ts +++ b/src/utils/twitter.ts @@ -14,7 +14,7 @@ const client = new Twitter({ async function tweet(content: string): Promise { if (content.length > 280) throw new Error('Status is longer than 280 characters.') - return await client.post('statuses/update', { + return client.post('statuses/update', { status: content }) } From a9d1d18085416747537bb8c51dd048a3d4b44e4b Mon Sep 17 00:00:00 2001 From: Reed Date: Wed, 30 Nov 2022 09:53:55 -0500 Subject: [PATCH 2/3] foundation for json config --- src/exceptions/NotImplementedException.ts | 5 +++++ src/types/HTCheck.type.ts | 9 +++++++++ src/types/HTCheckType.type.ts | 4 ++++ src/types/HTConfig.type.ts | 12 ++++++++++++ src/types/HTConfigFile.type.ts | 9 +++++++++ src/types/HTDB2Config.type.ts | 7 +++++++ src/utils/config-parser.ts | 6 ++++++ 7 files changed, 52 insertions(+) create mode 100644 src/exceptions/NotImplementedException.ts create mode 100644 src/types/HTCheck.type.ts create mode 100644 src/types/HTCheckType.type.ts create mode 100644 src/types/HTConfig.type.ts create mode 100644 src/types/HTConfigFile.type.ts create mode 100644 src/types/HTDB2Config.type.ts create mode 100644 src/utils/config-parser.ts diff --git a/src/exceptions/NotImplementedException.ts b/src/exceptions/NotImplementedException.ts new file mode 100644 index 0000000..07daf5e --- /dev/null +++ b/src/exceptions/NotImplementedException.ts @@ -0,0 +1,5 @@ +export default class NotImplementedException extends Error { + constructor() { + super('Method is not implemented') + } +} diff --git a/src/types/HTCheck.type.ts b/src/types/HTCheck.type.ts new file mode 100644 index 0000000..342a8eb --- /dev/null +++ b/src/types/HTCheck.type.ts @@ -0,0 +1,9 @@ +import { type HTCheckType } from "./HTCheckType.type" + +export type HTCheck = { + /** Type of check */ + type: HTCheckType, + + /** Params to pass to the check method. */ + params: any[] +} \ No newline at end of file diff --git a/src/types/HTCheckType.type.ts b/src/types/HTCheckType.type.ts new file mode 100644 index 0000000..1fba41f --- /dev/null +++ b/src/types/HTCheckType.type.ts @@ -0,0 +1,4 @@ +export type HTCheckType = + | 'common' + | 'stream' + | 'game' \ No newline at end of file diff --git a/src/types/HTConfig.type.ts b/src/types/HTConfig.type.ts new file mode 100644 index 0000000..957eb04 --- /dev/null +++ b/src/types/HTConfig.type.ts @@ -0,0 +1,12 @@ +import { type HTDB2Config } from "./HTDB2Config.type" + +export type HTConfig = { + /** Options to configure DB2 (the in-memory database) */ + db2: HTDB2Config + + /** The time to wait between checks. */ + timeoutMs: number, + + /** Configured checks to run. */ + checks: HTCheck[] +} \ No newline at end of file diff --git a/src/types/HTConfigFile.type.ts b/src/types/HTConfigFile.type.ts new file mode 100644 index 0000000..189ec1f --- /dev/null +++ b/src/types/HTConfigFile.type.ts @@ -0,0 +1,9 @@ +import { type HTConfig } from './HTConfig.type' + +export type HTConfigFile = { + /** Schema version */ + __v: number, + + /** Config root */ + config: HTConfig +} \ No newline at end of file diff --git a/src/types/HTDB2Config.type.ts b/src/types/HTDB2Config.type.ts new file mode 100644 index 0000000..130f38e --- /dev/null +++ b/src/types/HTDB2Config.type.ts @@ -0,0 +1,7 @@ +export type HTDB2Config = { + /** Whether or not to serialize the database to disk when the program starts */ + serializeOnBoot: boolean + + /** Cron string to determine how often the database gets serialized to disk. */ + serializeCronString: string +} \ No newline at end of file diff --git a/src/utils/config-parser.ts b/src/utils/config-parser.ts new file mode 100644 index 0000000..936fd57 --- /dev/null +++ b/src/utils/config-parser.ts @@ -0,0 +1,6 @@ +import NotImplementedException from '../exceptions/NotImplementedException' +import { type HTConfigFile } from '../types/HTConfigFile.type' + +export default async function getConfigFile (fileName: string = "tkrcfg.json") { + throw new NotImplementedException() +} \ No newline at end of file From 10ce56479ad8a04752a93156be75c0102484401f Mon Sep 17 00:00:00 2001 From: Reed Date: Mon, 16 Jan 2023 15:02:14 -0500 Subject: [PATCH 3/3] Control checks via JSON config. --- .gitignore | 3 +- src/exceptions/InvalidHTConfigException.ts | 5 ++ src/index.ts | 59 +++++++++++++++------- src/types/HTCheck.type.ts | 1 + src/types/HTConfig.type.ts | 2 +- src/types/HTDB2Config.type.ts | 6 +++ src/utils/config-parser.ts | 29 ++++++++++- tkrcfg.example.json | 49 ++++++++++++++++++ 8 files changed, 131 insertions(+), 23 deletions(-) create mode 100644 src/exceptions/InvalidHTConfigException.ts create mode 100644 tkrcfg.example.json diff --git a/.gitignore b/.gitignore index 6e0e84d..7f82705 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env tracker.json node_modules -dist/ \ No newline at end of file +dist/ +tkrcfg.json \ No newline at end of file diff --git a/src/exceptions/InvalidHTConfigException.ts b/src/exceptions/InvalidHTConfigException.ts new file mode 100644 index 0000000..8074b50 --- /dev/null +++ b/src/exceptions/InvalidHTConfigException.ts @@ -0,0 +1,5 @@ +export default class InvalidHTConfigException extends Error { + constructor() { + super('Invalid HTConfig file!') + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index cba46e6..c5cd376 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,42 +8,63 @@ import gameCheck from './checks/game.check.js' import { init, serializeDbToDisk } from './utils/db2.js' import cron from 'node-cron' -const timeoutTime = parseInt((process.env.TIMEOUT_TIME as string)) +import getConfig from './utils/config-parser.js' + +const { config } = await getConfig() + +const timeoutTime = config.timeoutMs let cycle = 0 debug('HypeTrack started on %s.', new Date()) // TODO: Web and Mesa. -// Initialize DB2. -await init() -await serializeDbToDisk() // Initially serialize a copy of the database to disk. +// Initialize DB2 if config says to do so. +config.db2.initializeOnBoot && await init() -// This cron job serializes the database to disk every five minutes. -cron.schedule('*/5 * * * *', async () => { - await serializeDbToDisk() -}) +if (config.db2.serializeOnBoot) { + await serializeDbToDisk() // Initially serialize a copy of the database to disk. +} + +if (typeof config.db2.serializeCronString === 'string') { + // This cron job serializes the database to disk every five minutes. + cron.schedule(config.db2.serializeCronString, async () => { + await serializeDbToDisk() + }) +} // This detects interrupts, and will serialize the DB to disk before breaking. // TODO: I don't think this works on Windows. process.on('SIGINT', async () => { - console.log('Interrupt caught! Serializing database to disk.') - await serializeDbToDisk() + console.log('Interrupt caught!') + if (config.db2.serializeOnInterrupt) { + console.log('Serializing database to disk') + await serializeDbToDisk() + } process.exit(0) }) -// TODO: Have a way to have this dynamically expandable. -// TODO: Figure out what needs priority here. +const commonChecks = config.checks.filter(check => check.type === 'common') +const streamChecks = config.checks.filter(check => check.type === 'stream') +const gameChecks = config.checks.filter(check => check.type === 'game') + +let checkCount = 0; +[commonChecks, streamChecks, gameChecks].forEach(checks => checkCount += checks.length) +debug('Starting %d checks.', checkCount) + setInterval(async () => { debug('We\'re on check cycle %d.', cycle) - await commonCheck('https://api.prod.hype.space', 'hypeapi') - await commonCheck('https://ws.prod.hype.space', 'hypeapi-websocket') - await commonCheck('https://telemetry.prod.hype.space', 'api-telemetry') - await streamCheck('internet_high') - await streamCheck('intranet_high') - await streamCheck('wirecast_high') + for (const check of commonChecks) { + await commonCheck(check.params[0], check.params[1]) + } + + for (const check of streamChecks) { + await streamCheck(check.params[0]) + } - await gameCheck() + if (gameChecks.length > 0) { + await gameCheck() + } // Increment cycle debug('Cycle %d ended.', cycle) diff --git a/src/types/HTCheck.type.ts b/src/types/HTCheck.type.ts index 342a8eb..837f5e9 100644 --- a/src/types/HTCheck.type.ts +++ b/src/types/HTCheck.type.ts @@ -5,5 +5,6 @@ export type HTCheck = { type: HTCheckType, /** Params to pass to the check method. */ + // rome-ignore lint/suspicious/noExplicitAny: Types can be anything on this, honestly. Too lazy to give it explicit types. params: any[] } \ No newline at end of file diff --git a/src/types/HTConfig.type.ts b/src/types/HTConfig.type.ts index 957eb04..74babcb 100644 --- a/src/types/HTConfig.type.ts +++ b/src/types/HTConfig.type.ts @@ -1,5 +1,5 @@ import { type HTDB2Config } from "./HTDB2Config.type" - +import { type HTCheck } from "./HTCheck.type" export type HTConfig = { /** Options to configure DB2 (the in-memory database) */ db2: HTDB2Config diff --git a/src/types/HTDB2Config.type.ts b/src/types/HTDB2Config.type.ts index 130f38e..516d5d6 100644 --- a/src/types/HTDB2Config.type.ts +++ b/src/types/HTDB2Config.type.ts @@ -1,7 +1,13 @@ export type HTDB2Config = { + /** Whether or not the database should be initialized on start of tracker. */ + initializeOnBoot: boolean + /** Whether or not to serialize the database to disk when the program starts */ serializeOnBoot: boolean /** Cron string to determine how often the database gets serialized to disk. */ serializeCronString: string + + /** Whether or not to serialize the DB to disk on interrupt. */ + serializeOnInterrupt: boolean } \ No newline at end of file diff --git a/src/utils/config-parser.ts b/src/utils/config-parser.ts index 936fd57..7f0b35a 100644 --- a/src/utils/config-parser.ts +++ b/src/utils/config-parser.ts @@ -1,6 +1,31 @@ -import NotImplementedException from '../exceptions/NotImplementedException' +import fs from 'node:fs/promises' +import InvalidHTConfigException from '../exceptions/InvalidHTConfigException' import { type HTConfigFile } from '../types/HTConfigFile.type' +let config: HTConfigFile + export default async function getConfigFile (fileName: string = "tkrcfg.json") { - throw new NotImplementedException() + if (config) { + return config + } + + // Open file. + const file = await fs.open(fileName, 'r') + + // Read file. + const fileContents = await file.readFile() + + // Parse file. + const parsedFile = JSON.parse(fileContents.toString()) as HTConfigFile + + // Check if file is valid. + if (!parsedFile) { + throw new InvalidHTConfigException() + } + + // Save config. + config = parsedFile + + // Return json. + return parsedFile } \ No newline at end of file diff --git a/tkrcfg.example.json b/tkrcfg.example.json new file mode 100644 index 0000000..8ca011f --- /dev/null +++ b/tkrcfg.example.json @@ -0,0 +1,49 @@ +{ + "__v": 1, + "config": { + "db2": { + "serializeOnBoot": true, + "serializeCronString": "*/5 * * * *" + }, + "timeoutMs": 5000, + "checks": [ + { + "type": "common", + "params": [ + "api.prod.hype.space", + "hypeapi" + ] + }, + { + "type": "common", + "params": [ + "ws.prod.hype.space", + "hypeapi-websocket" + ] + }, + { + "type": "common", + "params": [ + "telemetry.prod.hype.space", + "api-telemetry" + ] + }, + { + "type": "stream", + "params": ["internet_high"] + }, + { + "type": "stream", + "params": ["intranet_high"] + }, + { + "type": "stream", + "params": ["wirecast_high"] + }, + { + "type": "game", + "params": [] + } + ] + } +} \ No newline at end of file