initial commit

This commit is contained in:
Darius
2025-09-26 13:41:46 +02:00
commit 9a961363f3
17 changed files with 1553 additions and 0 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
PREFIX=prefixLike!
USERNAME=usernameOfBot
ACCESS_TOKEN=oauthAccessToken
CHANNELS=channelsOfBot
DEVELOPERS=userIdOfDeveloper

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.env
.env.*
!.env.example
node_modules
dist
package-lock.jsom

6
.zed/settings.json Normal file
View File

@@ -0,0 +1,6 @@
{
"code_actions_on_format": {
"source.organizeImports.biome": true,
"source.fixAll.biome": true
}
}

1289
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "twitch-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc -p .",
"start": "node dist/bot.js",
"dev": "nodemon src/bot.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@types/node": "^24.5.2",
"@types/tmi.js": "^1.8.6",
"nodemon": "^3.1.10",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
},
"dependencies": {
"@discordjs/collection": "^2.1.1",
"chalk": "^5.6.2",
"dotenv": "^17.2.2",
"tmi.js": "^1.8.5"
}
}

13
src/bot.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { Events } from "tmi.js";
import { client } from "./core/client.ts";
import { events } from "./events/collection.ts";
// Register all events with the TMI client
for (const [eventName, eventHandler] of events) {
client.on(
eventName as keyof Events,
eventHandler.triggered.bind(null, client),
);
}
client.connect();

View File

@@ -0,0 +1,7 @@
import { Collection } from "@discordjs/collection";
import { SongCommand } from "./command-song.ts";
import type { ICommand } from "./interface.ts";
export const commands = new Collection<string, ICommand>();
commands.set(SongCommand.name, new SongCommand());

View File

@@ -0,0 +1,23 @@
import type { ChatUserstate, Client } from "tmi.js";
import type { ICommand, ICommandRequirements } from "./interface.ts";
export class SongCommand implements ICommand {
name = "song";
cooldown = 0;
enabled = true;
requirements: ICommandRequirements = {
developer: true,
mod: false,
};
triggered = async (
client: Client,
channel: string,
_state: ChatUserstate,
_message: string,
_args: Array<string>,
) => {
client.say(channel, "testing");
};
}

12
src/commands/interface.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface ICommand {
name: string;
cooldown: number;
enabled: boolean;
triggered(...args: unknown[]): Promise<unknown>;
requirements: ICommandRequirements;
}
export interface ICommandRequirements {
developer: boolean;
mod: boolean;
}

11
src/config/config.ts Normal file
View File

@@ -0,0 +1,11 @@
import dotenv from "dotenv";
dotenv.config();
export const Config = {
prefix: process.env.PREFIX || "",
username: process.env.USERNAME || "",
access_token: process.env.ACCESS_TOKEN || "",
channels: [process.env.CHANNELS || ""],
developers: [process.env.DEVELOPERS || ""],
};

11
src/core/client.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Client } from "tmi.js";
import { Config } from "../config/config.ts";
export const client = new Client({
connection: { reconnect: true, secure: true },
identity: {
username: Config.username,
password: `oauth:${Config.access_token}`,
},
channels: Config.channels,
});

9
src/events/collection.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Collection } from "@discordjs/collection";
import ConnectedEvent from "./event-connected.ts";
import MessageEvent from "./event-message.ts";
import type { IEvent } from "./interface.ts";
export const events = new Collection<string, IEvent>();
events.set(ConnectedEvent.name, new ConnectedEvent());
events.set(MessageEvent.name, new MessageEvent());

View File

@@ -0,0 +1,11 @@
import type { Events } from "tmi.js";
import { logSuccess } from "../logger/logger.ts";
import type { IEvent } from "./interface.ts";
export default class ConnectedEvent implements IEvent {
name: keyof Events = "connected";
triggered = async () => {
logSuccess("connected");
};
}

View File

@@ -0,0 +1,78 @@
import { Collection } from "@discordjs/collection";
import type { ChatUserstate, Client, Events } from "tmi.js";
import { commands } from "../commands/collection.ts";
import type { ICommand } from "../commands/interface.ts";
import { Config } from "../config/config.ts";
import type { IEvent } from "./interface.ts";
const Cooldowns = new Collection<string, number>();
export default class MessageEvent implements IEvent {
name: keyof Events = "message";
triggered = async (
client: Client,
channel: string,
state: ChatUserstate,
message: string,
self: boolean,
) => {
if (self) return;
await checkMessage(client, channel, state, message);
};
}
async function checkMessage(
client: Client,
channel: string,
state: ChatUserstate,
message: string,
) {
const prefix = Config.prefix;
if (!message.startsWith(prefix)) return;
const args = message.slice(prefix.length).trim().split(/ +/g);
const commandName = args[0].toLowerCase();
const command = commands.get(commandName);
if (!command) return;
if (!command.enabled) return;
const userId = state["user-id"] as string;
if (command.requirements.developer && !isDeveloper(userId)) return;
if (command.requirements.mod && !isMod(state)) return;
const timeLeft = checkCooldown(command);
if (timeLeft > 0) {
return client.say(
channel,
`you must wait ${timeLeft} more seconds to use the command again`,
);
}
await command.triggered(client, channel, state, message, args);
}
function isDeveloper(userId: string): boolean {
return Config.developers.includes(userId);
}
function isMod(state: ChatUserstate): boolean {
return state.mod as boolean;
}
function checkCooldown(command: ICommand): number {
const now = Date.now();
if (command.cooldown > 0) {
const cooldownTime = Cooldowns.get(command.name);
if (cooldownTime) {
if (cooldownTime < now) {
const timeLeft = 0; // TODO
return timeLeft;
} else {
Cooldowns.set(command.name, now + command.cooldown * 1000);
}
}
}
return 0;
}

6
src/events/interface.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { Events } from "tmi.js";
export interface IEvent {
name: keyof Events;
triggered(...args: unknown[]): Promise<unknown>;
}

13
src/logger/logger.ts Normal file
View File

@@ -0,0 +1,13 @@
import chalk from "chalk";
export function logError(...args: unknown[]) {
console.log(chalk.red(args));
}
export function logSuccess(...args: unknown[]) {
console.log(chalk.green(args));
}
export function logInfo(...args: unknown[]) {
console.log(chalk.cyan(args));
}

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true,
"types": ["node"],
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"incremental": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}