Skip to content

Commit

Permalink
Add discord web hooks.
Browse files Browse the repository at this point in the history
  • Loading branch information
TychoTheTaco committed Jun 7, 2022
1 parent 107c33c commit 2159d3b
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 37 deletions.
63 changes: 46 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ You are using the `Command Prompt` command above, but are not using Command Prom
4) Build the app: `npm run build`
5) Start the bot with `node dist/index.js` or `npm run start`. If there is no configuration file, a default one will be created.
6) Customize your config.json. (Restart the bot for changes to be applied)
1) By default, the bot will attempt to watch all games. You can change which games that the bot watches by specifying game IDs in the config file. See `games.csv` for the game IDs.
2) Add your username to the config so that the bot can reuse the correct cookies, so you don't have to log in again everytime the bot is restarted.
1) By default, the bot will attempt to watch all games. You can change which games that the bot watches by specifying game IDs in the config file. See `games.csv` for the game IDs.
2) Add your username to the config so that the bot can reuse the correct cookies, so you don't have to log in again everytime the bot is restarted.

After updating your install, re-run `npm install .` and `npm run build`.

Expand All @@ -70,39 +70,56 @@ $ sudo sudo apt-get update && apt-get install -y nodejs

## Options

There are multiple options you can configure. They can be provided as command line arguments or in a config JSON file. Options passed as command line arguments will override items in the config file. If no command line arguments are provided, a default config file will be generated.
There are multiple options you can configure. They can be provided as command line arguments or in a config JSON file. Options passed as command line arguments will override items in the config file. If no command line arguments are
provided, a default config file will be generated.

A sample config file looks like this:

```
{
"username": "my_twitch_username",
"password": "my_twitch_password",
"browser": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"games": ["30921", "511224", "488552"],
"games": ["30921", "514152", "138585"],
"headless": true,
"headless_login": false,
"headless_login": true,
"interval": 15,
"load_timeout_secs": 30,
"failed_stream_retry": 3,
"failed_stream_timeout": 30,
"browser_args": [],
"watch_unlisted_games": false,
"hide_video": false,
"watch_unlisted_games": true,
"hide_video": true,
"show_account_not_linked_warning": true,
"ignored_games": [],
"ignored_games": ["509511"],
"attempt_impossible_campaigns": true,
"watch_streams_when_no_drop_campaigns_active": false,
"broadcasters": [],
"watch_streams_when_no_drop_campaigns_active": true,
"do_version_check": true,
"broadcasters": ["my_favorite_streamer", "my_second_favorite_streamer"],
"tui": {
"enabled": false
"enabled": true
},
"updates": {
"type": "release",
"enabled": true
},
"logging": {
"enabled": true,
"level": "debug"
"notifications": {
"discord": [
{
"webhook_url": "my_webhook_url",
"events": [
"new_drops_campaign", "drop_claimed"
],
"games": "all"
},
{
"webhook_url": "another_webhook_url",
"events": [
"new_drops_campaign"
],
"games": "config"
}
]
}
}
```
Expand All @@ -111,7 +128,7 @@ Below is a list of all available options.

`--config <path>` The path to your configuration file.

- Alias: `-c`
- Alias: `-c`
- Default: `config.json`

`--browser <path>` | `browser` The path to your browser executable. Only Google Chrome is currently supported. Although Puppeteer includes a version of Chromium, it does not support the video format required to watch Twitch streams, so a separate Google Chrome installation is required.
Expand Down Expand Up @@ -196,14 +213,26 @@ Below is a list of all available options.

- `enabled`: boolean - When `true` (default), the bot will check for updates once per day.
- `type`: string - Determines which type of update to be notified of.
- `"dev"` - Notify about updates for new development versions and release versions of the bot.
- `"release"` - (default) Only notify for new release versions of the bot.
- `"dev"` - Notify about updates for new development versions and release versions of the bot.
- `"release"` - (default) Only notify for new release versions of the bot.

`logging` - Change options related to logging. This should be in JSON format.

- `enabled`: When `true` (default), the app will log data to a file.
- `file`: Path of the log file (default: `log-XXXXXXXXXX.txt`). If you use this option, the file will be overwritten whenever you restart the app!
- `level`: The level of logging to write to the log file. One of: `debug` (default), `info`, `warn`, `error`.

`notifications` - Change options related to notifications. This should be in JSON format.

- `discord`: An array of discord notification objects. Each notification object has the following properties:
- `webhook_url`: The URL of the Discord webhook.
- `events`: A list of events to get notifications for. Possible events are:
- `drop_claimed`: A drop is claimed.
- `new_drops_campaign`: A new Drops campaign was found.
- `games`: Which games you want notifications for. Options are:
- `config`: Only get notifications related to games that are listed in the config.
- `all`: Get notifications related to any game.

### Update Games List

If you want to update the list of games found in `games.csv`, just run `npm run updateGames` or `npm run u`.
Expand Down
84 changes: 75 additions & 9 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import cliProgress from "cli-progress";
const {BarFormat} = cliProgress.Format;

import logger from './logger.js';
import {getDropBenefitNames, TimeBasedDrop} from './twitch.js';
import {DropCampaign, getDropBenefitNames, TimeBasedDrop} from './twitch.js';
import {StringOption, BooleanOption, IntegerOption, StringListOption, JsonOption} from './options.js';
import {TwitchDropsBot} from './twitch_drops_bot.js';
import {ConfigurationParser} from './configuration_parser.js';
import {LoginPage} from "./pages/login.js";
import {Application} from "./ui/ui.js";
import {compareVersionString, getLatestDevelopmentVersion, getLatestReleaseVersion} from "./utils.js";
import {transports} from "winston";
import {DiscordWebhookSender} from "./notifiers/discord.js";

// Load version number from package.json
let VERSION = "unknown";
Expand Down Expand Up @@ -323,9 +324,18 @@ const options = [
file: undefined,
level: 'debug'
}
})
}),
new JsonOption<{
discord: DiscordNotifier[]
}>("--notifications")
];

interface DiscordNotifier {
webhook_url: string,
events: ("new_drops_campaign" | "drop_claimed")[],
games: "all" | "config" // "config" = only notify for games explicitly listed in config. "all" = notify for all games (including watch_unlisted_games)
}

export interface Config {
username: string,
password?: string,
Expand Down Expand Up @@ -358,6 +368,9 @@ export interface Config {
enabled: boolean,
file: string,
level: "debug" | "info" | "warn" | "error"
},
notifications: {
discord: DiscordNotifier[]
}
}

Expand All @@ -373,13 +386,22 @@ function findMostRecentlyModifiedCookiesPath() {
const stats = fs.statSync(path);
if (!mrmTime || stats.mtime > mrmTime) {
mrmTime = stats.mtime;
mrmPath = path;
mrmPath = path;
}
}

return mrmPath;
}

/**
* Log some environment information that is useful for debugging.
*/
function logEnvironmentInfo() {
logger.debug("current system time: " + new Date());
logger.debug(`git commit hash: ${process.env.GIT_COMMIT_HASH}`);
logger.debug("NodeJS version: " + process.version);
}

async function main() {
// Parse arguments
const configurationParser = new ConfigurationParser(options);
Expand All @@ -400,21 +422,18 @@ async function main() {
}));
}

logEnvironmentInfo();

process.setUncaughtExceptionCaptureCallback(error => {
logger.error("Uncaught exception: " + error.stack);
});

// Log the current time
logger.debug("current system time: " + new Date());

// todo: move this into a validation step in the config parser
if (!["release", "dev"].includes(config.updates.type)) {
logger.error("Invalid update type: " + config.updates.type);
process.exit(1);
}

logger.debug(`git commit hash: ${process.env.GIT_COMMIT_HASH}`);

// Add default browser args
const defaultBrowserArgs = [
'--mute-audio',
Expand Down Expand Up @@ -488,7 +507,7 @@ async function main() {
args: config['browser_args']
});

// Automatically stop this program if the browser or page is closed
// Automatically stop this program if the browser is closed
browser.on('disconnected', onBrowserOrPageClosed);

// Check if we have saved cookies
Expand Down Expand Up @@ -600,6 +619,8 @@ async function main() {
broadcasterIds: config["broadcasters"]
});

setUpNotifiers(bot, config);

if (config.tui.enabled) {
startUiMode(bot, config);
} else {
Expand All @@ -609,6 +630,51 @@ async function main() {
await bot.start();
}

function setUpNotifiers(bot: TwitchDropsBot, config: Config) {

bot.on("new_drops_campaign_found", (campaign: DropCampaign) => {
for (const notifier of config.notifications.discord) {
if (notifier.events.includes("new_drops_campaign")) {
if (notifier.games === "config" && !config.games.includes(campaign.game.id)){
continue;
}
new DiscordWebhookSender(notifier.webhook_url).sendNewDropsCampaignWebhook(campaign).catch(error => {
logger.error("Failed to send Discord webhook!");
logger.debug(error);
});
}
}
});

bot.on("drop_claimed", (dropId => {
const drop = bot.getDatabase().getDropById(dropId);
if (!drop) {
logger.error("Failed to send Discord webhook: drop was null. id: " + dropId);
return;
}

const campaign = bot.getDatabase().getDropCampaignByDropId(dropId);
if (!campaign) {
logger.error("Failed to send Discord webhook: campaign was null");
logger.debug("drop: " + JSON.stringify(drop, null, 4));
return;
}

for (const notifier of config.notifications.discord) {
if (notifier.events.includes("drop_claimed")) {
if (notifier.games === "config" && !config.games.includes(campaign.game.id)){
continue;
}
new DiscordWebhookSender(notifier.webhook_url).sendDropClaimedWebhook(drop, campaign).catch(error => {
logger.error("Failed to send Discord webhook!");
logger.debug(error);
});
}
}

}));
}

// Check if this file is being run directly
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
Expand Down
102 changes: 102 additions & 0 deletions src/notifiers/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import axios from "axios";

import {DropCampaign, getDropBenefitNames, TimeBasedDrop} from "../twitch.js";

function formatTimestamp(timestamp: string) {
return new Date(timestamp).toLocaleString(undefined, {
timeStyle: "short",
dateStyle: "short"
});
}

function formatTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours} hr` + (hours === 1 ? '' : 's');
}
return `${minutes} min` + (minutes === 1 ? '' : 's');
}

export class DiscordWebhookSender {

readonly #webhookUrl: string;

constructor(webhookUrl: string) {
this.#webhookUrl = webhookUrl;
}

async sendNewDropsCampaignWebhook(campaign: DropCampaign) {
let dropsString = ""
const timeBasedDrops = campaign.timeBasedDrops;
if (timeBasedDrops) {
dropsString = timeBasedDrops.map(drop => {
return `**[${formatTime(drop.requiredMinutesWatched)}]** ${getDropBenefitNames(drop)}`;
}).join("\n");
}

const fields = [
{
name: "Game",
value: campaign.game.displayName
},
{
name: "Campaign",
value: campaign.name
},
{
name: "Starts",
value: `${formatTimestamp(campaign.startAt)}`
},
{
name: "Ends",
value: `${formatTimestamp(campaign.endAt)}`
}
];
if (dropsString.length > 0) {
fields.push({
name: "Drops",
value: dropsString
});
}

await axios.post(this.#webhookUrl, {
embeds: [
{
title: "New Drops Campaign",
fields: fields,
thumbnail: {
url: campaign.game.boxArtURL
}
}
]
});
}

async sendDropClaimedWebhook(drop: TimeBasedDrop, campaign: DropCampaign) {
await axios.post(this.#webhookUrl, {
embeds: [
{
title: "Drop Claimed",
fields: [
{
name: "Game",
value: campaign.game.displayName
},
{
name: "Campaign",
value: campaign.name
},
{
name: "Drop",
value: getDropBenefitNames(drop)
}
],
thumbnail: {
url: drop.benefitEdges[0].benefit.imageAssetURL
}
}
]
});
}

}
3 changes: 2 additions & 1 deletion src/twitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export interface TimeBasedDrop {
benefit: {
id: string,
name: string,
game: Game
game: Game,
imageAssetURL: string
}
}[],
startAt: string,
Expand Down
Loading

0 comments on commit 2159d3b

Please sign in to comment.