Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions notes/writing_checks.md
Original file line number Diff line number Diff line change
@@ -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<string>('someKeyHere')

if (typeof data === 'undefined') {
// Write a default value.
await set<string>('someKeyHere', 'abc')

// Return and come back next time
return
}
```

The `<string>` part of the statement tells DB2 how to get and write the data to DB2.
82 changes: 49 additions & 33 deletions src/checks/common.check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions src/checks/game.check.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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) {
Expand Down
63 changes: 36 additions & 27 deletions src/checks/stream.check.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<boolean>(`streamLive_${playlist}`)
const streamLive = await get<boolean>(db2Key)

try {
d('Attempting request to %s.m3u8...', playlist)
Expand All @@ -33,39 +62,19 @@ async function check(playlist: string) {
}

// Set streamLive for this playlist.
await set<boolean>(`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<boolean>(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<boolean>(`streamLive_${playlist}`, false)
await set<boolean>(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
Expand Down
2 changes: 2 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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