diff --git a/notes/writing_checks.md b/notes/writing_checks.md new file mode 100644 index 0000000..d1d37b0 --- /dev/null +++ b/notes/writing_checks.md @@ -0,0 +1,150 @@ +# Writing HypeTrack Checks + +HypeTrack checks are built to be as simple as possible to implement. + +## Import libraries + +In most cases, you'll need these at the top of your new check: +```ts +import axios from 'axios' +import debug from '../utils/debug.js' +import { tweet } from '../utils/twitter.js' +import { postToDiscord } 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' +``` + +Explanation for each import: +* `axios` + * Used for HTTP requests. (This will be changed eventually when Node.js gets native fetch support.) +* `../utils/debug.js` + * Used for visual debugging. This is the script that handles the base namespace for tracker's debug logs. +* `../utils/twitter.js` + * Helper script for tweeting. Handles the checks for tweets being too long as well. +* `../utils/discord.js` + * Helper script for posting to Discord. +* `../utils/telegram.js` + * Helper script for posting to Telegram. +* `../utils/db2.js` + * This one is critical! It handles writing and fetching data from the in-memory database (DB2.) +* `../types/HTCheckConfig.type.js` + * Used as a type for the `config` constant. + +## Set up config + +The `HTCheckConfig` type has types for telling the `socials` function where to post messages. + +You write the config like this: +```ts +const config: HTCheckConfig { + sendToDiscord: true, + sendToTelegram: true, + sendToTwitter: true +} +``` + +If you don't want the check to initiate a message to a specific service, change the specific value to `false`. + +In the event you need to do a one-off config option, you can handle it by making an inline [intersection type](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types). +```ts +const config: HTCheckConfig & { sendToMastodon: boolean } { + sendToDiscord: true, + sendToTelegram: true, + sendToTwitter: true, + sendToMastodon: true +} +``` + +## Set up messages + +While not strictly required, first-party HypeTrack checks use this construct to make it easier to change what is posted in certain places. + +It's easy, just make a constant called `messages` and set up your messages like this: +```ts +const messages = { + stringA: "Didn't plan to never land", + stringB: (arbitraryParam: string) => `Just never thought that we could ${arbitraryParam}` +} +``` + +For stringA, you'd reference it as `messages.stringA`. For stringB, you'll have to pass a value for arbitraryParam, so this would work: `messages.stringB("drown")`. + +> Note that because `arbitraryParam` is strictly typed as a string, you can only pass strings to it. This means that `messages.stringB(42)` wouldn't work. + +## Define socials function + +The `socials` function should be an asynchronous function that returns nothing. So the definition of it should be something like this: +```ts +async function socials (anyParametersNeededHere: any): void { + // ... +} +``` + +You'd use the options defined in `config` to determine what the socials function needs to do. + +### Posting to Discord + +You'd normally use the `postToDiscord` function exported from `../utils/discord.js`, but the full Discord.js client is exported as `client` in the event that embeds need to be made. + +```ts +if (config.sendToDiscord) { + await postToDiscord('This will be posted to Discord.') + + // Alternatively: + await client.send('This will be posted to Discord.') +} +``` + +### Posting to Telegram + +Use the `tg` function. + +```ts +if (config.sendToTelegram) { + await tg('This will be posted to Telegram.') +} +``` + +### Posting to Twitter + +Use the `tweet` function. + +> Note that you don't have to check how long the text you're tweeting is. The `tweet` function will throw if your text is too long. + +```ts +if (config.sendToTwitter) { + await tweet('This will be posted to Twitter.') +} +``` + +## Defining the check function + +The check function is asynchronous and returns nothing, so you'd do this: +```ts +async function check (anyParametersNeeded: any): void { + // ... +} +``` + +A couple things you should do in your checks: +* **Extend out the debug scope and assign it to a constant called `d`.** + * `const d = debug.extend("check")` +* Wrap your code in a try-catch block so you can handle errors. + +### Checking if a key exists in DB2 + +Most tracker checks use this pattern to check if a key exists in DB2: +```ts +const data = await get('someKeyHere') + +if (typeof data === 'undefined') { + // Write a default value. + await set('someKeyHere', 'abc') + + // Return and come back next time + return +} +``` + +The `` part of the statement tells DB2 how to get and write the data to DB2. \ No newline at end of file diff --git a/src/checks/common.check.ts b/src/checks/common.check.ts index c08b17d..69328bf 100644 --- a/src/checks/common.check.ts +++ b/src/checks/common.check.ts @@ -3,8 +3,8 @@ 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 { Message } from '@cryb/mesa' import { get, set } from '../utils/db2.js' + import type { HTCheckConfig } from '../types/HTCheckConfig.type.js' import type { HTHost } from '../types/HTHost.type.js' import type { HTRevisionHistory } from '../types/HTRevisionHistory.type.js' @@ -15,6 +15,52 @@ const config: HTCheckConfig = { sendToTwitter: true } +const messages = { + // Discord specific + discordEmbedContent: 'Hear ye hear ye, new API', + discordEmbedTitle: (hostname: string) => `${hostname} Hash Changed`, + discordEmbedBody: (hostname: string) => `\`${hostname}\`'s hash has changed!`, + discordOldRevision: 'Old Hash', + discordNewRevision: 'New Hash', + + // Twitter / Telegram stuff + revisionChanged: (hostname: string, oldRevision: string, newRevision: string) => `${hostname}'s hash has changed!\n\n${oldRevision} => ${newRevision}` +} + +async function socials (hostname: string, oldRevision: string, newRevision: string) { + if (config.sendToDiscord) { + await client.send({ + content: messages.discordEmbedContent, + embeds: [{ + title: messages.discordEmbedTitle(hostname), + description: messages.discordEmbedBody(hostname), + color: 'RANDOM', + fields: [ + { + name: messages.discordOldRevision, + value: `\`${oldRevision}\``, + inline: true + }, + { + name: messages.discordNewRevision, + value: `\`${newRevision}\``, + inline: true + } + ], + timestamp: new Date() + }] + }) + } + + if (config.sendToTelegram) { + await tg(messages.revisionChanged(hostname, oldRevision, newRevision)) + } + + if (config.sendToTwitter) { + await tweet(messages.revisionChanged(hostname, oldRevision, newRevision)) + } +} + /** * Splits a hostname into an HTHost object. * @param {string} hostname Hostname returned from an axios.head() request. @@ -77,37 +123,7 @@ async function check(apiHost: string, friendlyHostname: string) { // If we get an undefined value here, there is nothing in the revision history and we've probably hit a new hash. d('The localRev and remoteRev do not match, and there is no match in the revisionHistory.') - if (config.sendToDiscord) { - await client.send({ - content: 'Hear ye hear ye, new API', - embeds: [{ - title: `${friendlyHostname} Changed`, - description: `\`${friendlyHostname}\`'s revision has changed! This could indicate a scale-up or new API changes.`, - color: 'RANDOM', - fields: [ - { - name: 'Old Revision', - value: `\`${localRev}\``, - inline: true - }, - { - name: 'New Revision', - value: `\`${rev}\``, - inline: true - } - ], - timestamp: new Date() - }] - }) - } - - if (config.sendToTelegram) { - await tg(`${friendlyHostname}'s revision has changed! This could indicate a scale-up or new API changes.\n\nOld Revision: ${localRev}\nNew Revision: ${rev}\nDate: ${new Date()}`) - } - - if (config.sendToTwitter) { - await tweet(`🛠️ I've detected that ${friendlyHostname}'s revision has changed. This could indicate an API change or a scale-up.\n\nOld revision: ${localRev}\nNew revision: ${rev}`) - } + await socials(friendlyHostname, localRev, rev) // To ensure we don't send out duplicates, create a new HTRevisionHistory object and append it to the revHistory. // Then we write this back. @@ -116,7 +132,7 @@ async function check(apiHost: string, friendlyHostname: string) { rev } - // See note at L77-78 for the reason the non-null assertion operator is used here. + // See note at L118-119 for the reason the non-null assertion operator is used here. revHistory!.push(newHistoryEntry) await set('revisionHistory', revHistory) diff --git a/src/checks/game.check.ts b/src/checks/game.check.ts index 4380b16..1fdc84e 100644 --- a/src/checks/game.check.ts +++ b/src/checks/game.check.ts @@ -1,7 +1,7 @@ import axios from 'axios' import debug from '../utils/debug.js' import { tweet } from '../utils/twitter.js' -import { client } from '../utils/discord.js' +import { postToDiscord } from '../utils/discord.js' import { tg } from '../utils/telegram.js' import { get, set } from '../utils/db2.js' @@ -16,15 +16,15 @@ const config: HTCheckConfig = { } const messages = { - gameLive: `An HQ game is active. (ts: ${+new Date()})`, - gameOver: `HQ is no longer active. (ts: ${+new Date()})` + 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 + let text = gameLive ? messages.gameLive() : messages.gameOver() if (config.sendToDiscord) { - await client.send(text) + await postToDiscord(text) } if (config.sendToTwitter) { diff --git a/src/checks/stream.check.ts b/src/checks/stream.check.ts index c5066f6..3d481f4 100644 --- a/src/checks/stream.check.ts +++ b/src/checks/stream.check.ts @@ -1,5 +1,8 @@ -import dotenv from 'dotenv' -dotenv.config() +// TODO: Why is this here? +// IIRC this was because of some weirdness with the Twitter utils script not finding its keys. +// But game/common works without this. +// import dotenv from 'dotenv' +// dotenv.config() import axios from 'axios' import debug from '../utils/debug.js' @@ -9,17 +12,43 @@ import { tg } from '../utils/telegram.js' import { get, set } from '../utils/db2.js' import type { HTCheckConfig } from '../types/HTCheckConfig.type.js' +const key = (playlist: string) => `streamLive_${playlist}` + const config: HTCheckConfig = { sendToDiscord: true, sendToTelegram: true, sendToTwitter: true } +const messages = { + streamLive: (playlist: string) => `The HQ stream is live @ https://hls.prod.hype.space/${playlist}.m3u8 (ts: ${Date.now()})`, + streamDown: (playlist: string) => `The HQ stream (${playlist}) is now down. (ts: ${Date.now()})` +} + +async function socials (playlist: string, streamLive: boolean) { + const text = streamLive + ? messages.streamLive(playlist) + : messages.streamDown(playlist) + + if (config.sendToDiscord) { + await postToDiscord(text) + } + + if (config.sendToTwitter) { + await tweet(text) + } + + if (config.sendToTelegram) { + await tg(text) + } +} + async function check(playlist: string) { const d = debug.extend(playlist) + const db2Key = key(playlist) // Get the already existing streamLive entry in our in-memory database. - const streamLive = await get(`streamLive_${playlist}`) + const streamLive = await get(db2Key) try { d('Attempting request to %s.m3u8...', playlist) @@ -33,39 +62,19 @@ async function check(playlist: string) { } // Set streamLive for this playlist. - await set(`streamLive_${playlist}`, true) - - if (config.sendToDiscord) { - await postToDiscord(`The HQ stream is live @ https://hls.prod.hype.space/${playlist}.m3u8 (ts: ${Date.now()})`) - } - - if (config.sendToTelegram) { - await tg(`The HQ stream is live @ https://hls.prod.hype.space/${playlist}.m3u8 (ts: ${Date.now()})`) - } + await set(db2Key, true) - if (config.sendToTwitter) { - await tweet(`The HQ stream is live @ https://hls.prod.hype.space/${playlist}.m3u8 (ts: ${Date.now()})`) - } + await socials(playlist, true) } catch (error: any) { d('%s is not live!', playlist) - await set(`streamLive_${playlist}`, false) + await set(db2Key, false) if (streamLive === true) { // The stream has gone offline since our last check. d('%s went offline since last check!', playlist) - if (config.sendToDiscord) { - await postToDiscord(`The HQ stream (${playlist}) is now down. (ts: ${Date.now()})`) - } - - if (config.sendToTelegram) { - await tg(`The HQ stream (${playlist}) is now down. (ts: ${Date.now()})`) - } - - if (config.sendToTwitter) { - await tweet(`The HQ stream (${playlist}) is now down. (ts: ${Date.now()})`) - } + await socials(playlist, false) } return diff --git a/start.sh b/start.sh index 1c47b17..7092cf5 100755 --- a/start.sh +++ b/start.sh @@ -7,4 +7,6 @@ if [ ! -d "$BUILD_DIR/" ]; then exit 1 fi +# Alternatively, you can remove the -db2 from this argument list. +# The only reason it is here is because DB2 is especially noisy. DEBUG=*,-follow-redirects,-telegraf:client,-db2 node dist/index