Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
thymue committed Jun 13, 2021
1 parent 9557d44 commit d56de6f
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package-lock.json
config.json
build/
node_modules/
11 changes: 11 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Remove all files from ./build/
Get-ChildItem -File -Recurse -Path "./build/" | `
ForEach-Object {
Remove-Item -Path $_.FullName;
}

# Compile typescript
tsc

# Package the app
pkg --out-path ./build ./build/src/index.js
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "tmi-public-npbot",
"version": "2.0.1",
"description": "grabs now playing map from gosumemory and displays it in chat upon request",
"main": "./src/index.ts",
"scripts": {
"run": "ts-node ./src/index.ts",
"build": "powershell.exe -File build.ps1"
},
"keywords": [
"tmi.js",
"twitch",
"osu",
"now-playing",
"chat-bot",
"twitch-chat-bot",
"np"
],
"author": "thymuue",
"license": "ISC",
"dependencies": {
"@types/node": "^15.12.2",
"@types/tmi.js": "^1.7.1",
"@types/websocket": "^1.0.2",
"tmi.js": "^1.8.3",
"websocket": "^1.0.34"
}
}
59 changes: 59 additions & 0 deletions src/GOsuMemoryHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { client as WebSocketClient, connection as WebSocketConnection, IMessage as IWebSocketMessage } from "websocket";

export default class GOsuMemoryHook
{
public npBeatmapId: number = -31;
public die: boolean = false;
private gOsuWebSocket: WebSocketClient;
private port: number;

constructor(port: number)
{
console.log(`[${new Date().toLocaleTimeString()}] Initializing gosumemory websocket...`);
this.gOsuWebSocket = new WebSocketClient();
this.port = port;

this.gOsuWebSocket.on("connect", (webSocketConnection: WebSocketConnection) => {
console.log(`[${new Date().toLocaleTimeString()}] Successfully connected to gosumemory websocket.`);
webSocketConnection.on("message", (webSocketMessage: IWebSocketMessage) => this.handleWebSocketMessage(webSocketMessage));
webSocketConnection.on("error", (error: Error) => this.handleWebSocketError(error));
});
this.gOsuWebSocket.on("connectFailed", (error: Error) => this.handleWebSocketError(error));

this.gOsuWebSocket.connect(`ws://127.0.0.1:${port}/ws`);
}

handleWebSocketMessage(webSocketMessage: IWebSocketMessage)
{
this.npBeatmapId = JSON.parse(webSocketMessage.utf8Data).menu.bm.id;
}

handleWebSocketError(e: Error)
{
if(this.die) return;
if(e.message.indexOf("ECONNREFUSED") > -1)
{
console.error(`[${new Date().toLocaleTimeString()}] ERROR: Couldn't connect to gosumemory websocket are you sure it's running? Retrying in 5 seconds.`);

setTimeout(() =>
{
this.gOsuWebSocket.connect(`ws://127.0.0.1:${this.port}/ws`);
}, 5000);
}
else if(e.message.indexOf("ECONNRESET") > -1)
{
this.npBeatmapId = -31;
console.error(`[${new Date().toLocaleTimeString()}] ERROR: Seems like gosumemory websocket stopped working, we'll try to reconnect in 5 seconds.`);

setTimeout(() =>
{
this.gOsuWebSocket.connect(`ws://127.0.0.1:${this.port}/ws`);
}, 5000);
}
else
{
console.error(`[${new Date().toLocaleTimeString()}] ERROR: Unrecognized error has occured. You can probably just restart the application and it should be working again.`);
console.error(`${e.name}\n${e.message}\n${e.stack}`);
}
}
}
11 changes: 11 additions & 0 deletions src/INpBotConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default interface INpBotConfiguration
{
botUsername: string,
botPassword: string,
twitchChannelName: string,
gOsuMemoryPort?: number
npCommand: string
useActionInsteadOfMessage: boolean
npMessage: string
npGOsuMemoryNotReadyMessage?: string
}
89 changes: 89 additions & 0 deletions src/NpBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ChatUserstate, Client as TmiClient } from "tmi.js";
import * as readline from "readline";
import GOsuMemoryHook from "./GOsuMemoryHook";
import INpBotConfiguration from "./INpBotConfiguration";

export default class NpBot
{
private client: TmiClient;
private gOsuMemoryHook: GOsuMemoryHook;
private npBotConfiguration: INpBotConfiguration;

constructor(npBotConfiguration: INpBotConfiguration)
{
console.log(`[${new Date().toLocaleTimeString()}] Initializing Twitch chat bot...`);
this.client = new TmiClient({
identity: {
username: npBotConfiguration.botUsername,
password: npBotConfiguration.botPassword
}
});
this.gOsuMemoryHook = new GOsuMemoryHook(npBotConfiguration.gOsuMemoryPort);

this.npBotConfiguration = npBotConfiguration;

this.client.on("join", (channel, username, self) =>
{
if(self) console.log(`[${new Date().toLocaleTimeString()}] Successfully joined twitch chat channel ${channel} as ${username}.`);
});
this.client.on("message", (channel, userstate, message, self) => this.handleMessage(channel, userstate, message, self));
this.client.on("notice", (channel, msgid, message) => {console.log(message)});

this.client.connect().catch((error: string) => this.handleConnectionError(error)).then(() => {
this.client.join(npBotConfiguration.twitchChannelName).catch((error: string) => this.handleConnectionError(error));
});
}

handleMessage(channel: string, userstate: ChatUserstate, message: string, self: boolean)
{
if(self) return;

if(message.toLowerCase() === this.npBotConfiguration.npCommand.toLowerCase())
{
if(this.gOsuMemoryHook.npBeatmapId == -31)
{
if(this.npBotConfiguration.npGOsuMemoryNotReadyMessage != null)
this.client.say(channel, this.npBotConfiguration.npGOsuMemoryNotReadyMessage.replace("{{username}}", userstate.username));
}
else
{
const npMessage = this.npBotConfiguration.npMessage
.replace("{{username}}", userstate.username)
.replace("{{beatmapLink}}", `https://osu.ppy.sh/b/${this.gOsuMemoryHook.npBeatmapId}`);
if(this.npBotConfiguration.useActionInsteadOfMessage)
this.client.action(channel, npMessage);
else
this.client.say(channel, npMessage);
}
}
}

handleConnectionError(error: string)
{
// If GOsuMemoryHook is marked as dieded this should also die
if(this.gOsuMemoryHook.die) return;
if(error.indexOf("Login authentication failed") > -1)
{
console.error(`[${new Date().toLocaleTimeString()}] Failed to log in, please make sure your botUsername and botPassword in config.json are correct.`);
const readLineInterface = readline.createInterface(process.stdin, process.stdout);
this.gOsuMemoryHook.die = true;
readLineInterface.question("", () => {
process.exit(0);
});
}
else if(error.indexOf("No response from Twitch.") > -1)
{
console.error(`[${new Date().toLocaleTimeString()}] Failed to join channel, please make sure your twitchChannelName in config.json is correct.`);
const readLineInterface = readline.createInterface(process.stdin, process.stdout);
this.gOsuMemoryHook.die = true;
readLineInterface.question("", () => {
process.exit(0);
});
}
else
{
console.error(`[${new Date().toLocaleTimeString()}] ERROR: Unrecognized error has occured.`);
console.error(error);
}
}
}
55 changes: 55 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// TODO: Make process.exits(0) just pause the app indefinitely or something because now it just exits the application without giving them a chance to read anything
import { existsSync, readFileSync, writeFileSync } from "fs";
import * as readline from "readline";
import INpBotConfiguration from "./INpBotConfiguration";
import NpBot from "./NpBot";

// Load configuration
let npBotConfiguration: INpBotConfiguration;
if(existsSync(`./config.json`))
{
npBotConfiguration = JSON.parse(readFileSync(`./config.json`).toString());
}
else
{
npBotConfiguration = {
botUsername: "",
botPassword: "",
twitchChannelName: "",
gOsuMemoryPort: 24050,
npCommand: "!np",
useActionInsteadOfMessage: true,
npMessage: "@{{username}}, here u go: {{beatmapLink}}",
npGOsuMemoryNotReadyMessage: "gosumemory died Sadge, please restart me"
}
writeFileSync(`./config.json`, JSON.stringify(npBotConfiguration, null, "\t"));
}

// Check if every required value is defined
const requiredValues = ["botUsername", "botPassword", "twitchChannelName", "npCommand", "npMessage"]
let exitAfter = false;
for(const requiredValue in requiredValues)
{
if(npBotConfiguration[requiredValues[requiredValue]] == undefined || npBotConfiguration[requiredValues[requiredValue]] == "")
{
console.error(`You have to set a value for ${requiredValues[requiredValue]} in config.json`);
exitAfter = true;
}
}
if(exitAfter)
{
const readLineInterface = readline.createInterface(process.stdin, process.stdout);
readLineInterface.question("", () => {
process.exit(0);
});
}
else
{
// Define defaults if optional values are undefined
if(npBotConfiguration.gOsuMemoryPort == undefined) npBotConfiguration.gOsuMemoryPort = 24050;
if(npBotConfiguration.npGOsuMemoryNotReadyMessage == undefined) npBotConfiguration.npGOsuMemoryNotReadyMessage = null;
if(npBotConfiguration.useActionInsteadOfMessage == undefined) npBotConfiguration.useActionInsteadOfMessage = true;

// Run the bot
const npBot = new NpBot(npBotConfiguration);
}
22 changes: 22 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"noImplicitAny": false,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "build/src/",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*",
"src/types/*"
]
}
},
"include": [
"src/**/*"
]
}

0 comments on commit d56de6f

Please sign in to comment.