add easy setup
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { client } from "./core/client.ts";
|
||||
import { chatClient } from "./core/chat-client.ts";
|
||||
import { registerAllEvents } from "./events/registry.ts";
|
||||
|
||||
registerAllEvents(client);
|
||||
client.connect();
|
||||
registerAllEvents();
|
||||
chatClient.connect();
|
||||
|
||||
14
src/commands/base-command.ts
Normal file
14
src/commands/base-command.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { chatClient } from "../core/chat-client.ts";
|
||||
import type { ICommand, ICommandRequirements } from "./interface";
|
||||
|
||||
export abstract class BaseCommand implements ICommand {
|
||||
protected get chatClient() {
|
||||
return chatClient;
|
||||
}
|
||||
|
||||
abstract name: string;
|
||||
abstract cooldown: number;
|
||||
abstract enabled: boolean;
|
||||
abstract triggered(...args: unknown[]): Promise<unknown>;
|
||||
abstract requirements: ICommandRequirements;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ChatMessage } from "@twurple/chat";
|
||||
import { logSuccess } from "../logger/logger.ts";
|
||||
import type { ICommand, ICommandRequirements } from "./interface.ts";
|
||||
import { logSuccess } from "../util/logger.ts";
|
||||
import { BaseCommand } from "./base-command.ts";
|
||||
import type { ICommandRequirements } from "./interface.ts";
|
||||
|
||||
export class SongCommand implements ICommand {
|
||||
export class SongCommand extends BaseCommand {
|
||||
name = "song";
|
||||
cooldown = 0;
|
||||
enabled = true;
|
||||
|
||||
@@ -7,6 +7,6 @@ export const Config = {
|
||||
bot_user_id: process.env.BOT_USER_ID || "",
|
||||
client_id: process.env.CLIENT_ID || "",
|
||||
client_secret: process.env.CLIENT_SECRET || "",
|
||||
channels: [process.env.CHANNELS || ""],
|
||||
developers: [process.env.DEVELOPERS || ""],
|
||||
channels: process.env.CHANNELS?.split(",") || [],
|
||||
developers: process.env.DEVELOPERS?.split(",") || [],
|
||||
};
|
||||
|
||||
10
src/core/api-client.ts
Normal file
10
src/core/api-client.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiClient } from "@twurple/api";
|
||||
import { AppTokenAuthProvider } from "@twurple/auth";
|
||||
import { Config } from "../config/config";
|
||||
|
||||
const authProvider = new AppTokenAuthProvider(
|
||||
Config.client_id,
|
||||
Config.client_secret,
|
||||
);
|
||||
|
||||
export const apiClient = new ApiClient({ authProvider });
|
||||
29
src/core/chat-client.ts
Normal file
29
src/core/chat-client.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { type AccessToken, RefreshingAuthProvider } from "@twurple/auth";
|
||||
import { ChatClient } from "@twurple/chat";
|
||||
import { Config } from "../config/config.ts";
|
||||
import { logError } from "../util/logger.ts";
|
||||
import { TokenManager } from "./token-manager.ts";
|
||||
|
||||
const tokenManager = new TokenManager(Config.bot_user_id);
|
||||
const tokenData = await tokenManager.getToken();
|
||||
|
||||
if (!tokenData) {
|
||||
logError("no token found");
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const authProviderPrivate = new RefreshingAuthProvider({
|
||||
clientId: Config.client_id,
|
||||
clientSecret: Config.client_secret,
|
||||
});
|
||||
|
||||
authProviderPrivate.onRefresh(async (_userId, newTokenData: AccessToken) =>
|
||||
tokenManager.updateToken(newTokenData),
|
||||
);
|
||||
|
||||
authProviderPrivate.addUser(Config.bot_user_id, tokenData, ["chat"]);
|
||||
|
||||
export const chatClient = new ChatClient({
|
||||
authProvider: authProviderPrivate,
|
||||
channels: Config.channels,
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { RefreshingAuthProvider } from "@twurple/auth";
|
||||
import { ChatClient } from "@twurple/chat";
|
||||
import { Config } from "../config/config.ts";
|
||||
|
||||
// Auth Token Stuff
|
||||
const tokenData = JSON.parse(
|
||||
await fs.readFile(`./ tokens.${Config.bot_user_id}.json`, "utf-8"),
|
||||
);
|
||||
|
||||
const authProvider = new RefreshingAuthProvider({
|
||||
clientId: Config.client_id,
|
||||
clientSecret: Config.client_secret,
|
||||
});
|
||||
|
||||
authProvider.onRefresh(
|
||||
async (_userId, newTokenData) =>
|
||||
await fs.writeFile(
|
||||
`./tokens.${Config.bot_user_id}.json`,
|
||||
JSON.stringify(newTokenData, null, 4),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
|
||||
await authProvider.addUserForToken(tokenData, ["chat"]);
|
||||
|
||||
export const client = new ChatClient({
|
||||
authProvider,
|
||||
channels: Config.channels,
|
||||
});
|
||||
32
src/core/token-manager.ts
Normal file
32
src/core/token-manager.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as readline from "node:readline";
|
||||
import type { AccessToken } from "@twurple/auth";
|
||||
|
||||
export class TokenManager {
|
||||
private readonly tokenFilePath: string;
|
||||
|
||||
constructor(userId: string) {
|
||||
this.tokenFilePath = `./tokens/${userId}.json`;
|
||||
}
|
||||
|
||||
async getToken(): Promise<AccessToken | null> {
|
||||
try {
|
||||
const tokenFile = await fs.readFile(this.tokenFilePath, "utf-8");
|
||||
return JSON.parse(tokenFile);
|
||||
} catch (_error) {
|
||||
console.log(
|
||||
`Token file ${this.tokenFilePath} not found. Please run the setup`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateToken(token: AccessToken): Promise<void> {
|
||||
this.createTokenFile(token);
|
||||
}
|
||||
|
||||
private async createTokenFile(tokenData: AccessToken): Promise<void> {
|
||||
const formattedTokens = JSON.stringify(tokenData, null, 4);
|
||||
await fs.writeFile(this.tokenFilePath, formattedTokens, "utf-8");
|
||||
}
|
||||
}
|
||||
12
src/events/base-event.ts
Normal file
12
src/events/base-event.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { chatClient } from "../core/chat-client.ts";
|
||||
import type { IEvent } from "./interface.ts";
|
||||
import type { EventName } from "./registry.ts";
|
||||
|
||||
export abstract class BaseEvent implements IEvent {
|
||||
protected get chatClient() {
|
||||
return chatClient;
|
||||
}
|
||||
|
||||
abstract name: EventName;
|
||||
abstract triggered(...args: unknown[]): Promise<unknown>;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { logSuccess } from "../logger/logger.ts";
|
||||
import type { IEvent } from "./interface.ts";
|
||||
import { logSuccess } from "../util/logger.ts";
|
||||
import { BaseEvent } from "./base-event.ts";
|
||||
import type { EventName } from "./registry.ts";
|
||||
|
||||
export default class ConnectedEvent implements IEvent {
|
||||
export default class ConnectedEvent extends BaseEvent {
|
||||
name: EventName = "connected";
|
||||
|
||||
triggered = async () => {
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { ChatMessage } from "@twurple/chat";
|
||||
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";
|
||||
import { BaseEvent } from "./base-event.ts";
|
||||
import type { EventName } from "./registry.ts";
|
||||
|
||||
const Cooldowns = new Collection<string, number>();
|
||||
|
||||
export default class MessageEvent implements IEvent {
|
||||
export default class MessageEvent extends BaseEvent {
|
||||
name: EventName = "message";
|
||||
|
||||
triggered = async (
|
||||
@@ -45,7 +45,7 @@ async function checkMessage(
|
||||
|
||||
const timeLeft = checkCooldown(command);
|
||||
if (timeLeft > 0) {
|
||||
// return client.say(
|
||||
// return chatClient.say(
|
||||
// channel,
|
||||
// `@${user}, you must wait ${timeLeft} more seconds to use the command again`,
|
||||
// );
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import type { ChatClient } from "@twurple/chat";
|
||||
import { chatClient } from "../core/chat-client.ts";
|
||||
import ConnectedEvent from "./event-connected.ts";
|
||||
import MessageEvent from "./event-message.ts";
|
||||
import type { IEvent } from "./interface.ts";
|
||||
|
||||
export const eventRegistry = {
|
||||
message: (client: ChatClient, handler: IEvent) =>
|
||||
client.onMessage(handler.triggered),
|
||||
connected: (client: ChatClient, handler: IEvent) =>
|
||||
client.onConnect(handler.triggered),
|
||||
message: (handler: IEvent) => chatClient.onMessage(handler.triggered),
|
||||
connected: (handler: IEvent) => chatClient.onConnect(handler.triggered),
|
||||
};
|
||||
|
||||
export function registerAllEvents(client: ChatClient) {
|
||||
export function registerAllEvents() {
|
||||
const events = [new MessageEvent(), new ConnectedEvent()];
|
||||
|
||||
for (const event of events) {
|
||||
const registerFn = eventRegistry[event.name];
|
||||
if (registerFn) {
|
||||
registerFn(client, event);
|
||||
}
|
||||
eventRegistry[event.name](event); // Registers the event
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
src/setup.ts
Normal file
47
src/setup.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { exchangeCode } from "@twurple/auth";
|
||||
import { Config } from "./config/config";
|
||||
import { TokenManager } from "./core/token-manager";
|
||||
import { TwitchAuth } from "./util/auth";
|
||||
import { getUserId } from "./util/general";
|
||||
import { logError, logInfo, logWarning } from "./util/logger";
|
||||
|
||||
const botname = "";
|
||||
const developers = [""];
|
||||
const scopes = ["chat:read", "chat:edit", "channel:moderate"];
|
||||
const port = 3000;
|
||||
const redirectUri = `http://localhost:${port}`;
|
||||
|
||||
const botId = await getUserId(botname);
|
||||
if (!botId) {
|
||||
logError("bot not found. please check the configuration");
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const developerIds = [];
|
||||
for (const dev in developers) {
|
||||
const devId = await getUserId(dev);
|
||||
if (devId) {
|
||||
developerIds.push(devId);
|
||||
} else {
|
||||
logWarning(`dev '${dev}' not found. please check the configuration`);
|
||||
}
|
||||
}
|
||||
|
||||
logInfo(`Userid of bot '${botname}' => '${botId}'`);
|
||||
logInfo(
|
||||
`Userids of developers '${developers.join(",")}' => '${developerIds.join(",")}'`,
|
||||
);
|
||||
|
||||
logInfo("--------");
|
||||
|
||||
const auth = new TwitchAuth(redirectUri);
|
||||
const authUrl = auth.getAuthorizationUrl(scopes);
|
||||
logInfo("To authorize your Twitch bot, visit this URL:");
|
||||
logInfo(authUrl);
|
||||
const code = await auth.startCallbackServer(port, 120);
|
||||
|
||||
const tokenManager = new TokenManager(botId);
|
||||
|
||||
tokenManager.updateToken(
|
||||
await exchangeCode(Config.client_id, Config.client_secret, code, redirectUri),
|
||||
);
|
||||
97
src/util/auth.ts
Normal file
97
src/util/auth.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
createServer,
|
||||
type IncomingMessage,
|
||||
type ServerResponse,
|
||||
} from "node:http";
|
||||
import { URL } from "node:url";
|
||||
import { Config } from "../config/config.js";
|
||||
|
||||
export class TwitchAuth {
|
||||
private readonly clientId: string;
|
||||
private readonly redirectUri: string;
|
||||
|
||||
constructor(redirectUri: string = "http://localhost:3000") {
|
||||
this.clientId = Config.client_id;
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
getAuthorizationUrl(scopes: string[]): string {
|
||||
const baseUrl = "https://id.twitch.tv/oauth2/authorize";
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
client_id: this.clientId,
|
||||
redirect_uri: this.redirectUri,
|
||||
scope: scopes.join(" "),
|
||||
});
|
||||
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
startCallbackServer(port: number, timeoutS: number = 120): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer(
|
||||
(req: IncomingMessage, res: ServerResponse) => {
|
||||
if (!req.url) {
|
||||
res.writeHead(400, { "Content-Type": "text/html" });
|
||||
res.end("<h1>Bad Request</h1><p>Invalid request URL</p>");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url, `http://localhost:${port}`);
|
||||
const code = url.searchParams.get("code");
|
||||
const error = url.searchParams.get("error");
|
||||
const errorDescription = url.searchParams.get("error_description");
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, { "Content-Type": "text/html" });
|
||||
res.end(`
|
||||
<h1>Authorization Failed</h1>
|
||||
<p>Error: ${error}</p>
|
||||
<p>Description: ${errorDescription || "Unknown error"}</p>
|
||||
<p>You can close this window.</p>
|
||||
`);
|
||||
server.close();
|
||||
reject(
|
||||
new Error(`Authorization failed: ${error} - ${errorDescription}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
res.writeHead(200, { "Content-Type": "text/html" });
|
||||
res.end(`
|
||||
<h1>Authorization Successful!</h1>
|
||||
<p>You have successfully authorized the Twitch bot.</p>
|
||||
<p>You can close this window and return to your terminal.</p>
|
||||
`);
|
||||
server.close();
|
||||
resolve(code);
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(400, { "Content-Type": "text/html" });
|
||||
res.end("<h1>Bad Request</h1><p>No authorization code received</p>");
|
||||
},
|
||||
);
|
||||
|
||||
server.listen(port, "localhost", () => {
|
||||
console.log(
|
||||
`Waiting for authorization callback on http://localhost:${port}`,
|
||||
);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(new Error(`Server error: ${err.message}`));
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
server.close();
|
||||
reject(new Error("Authorization timeout. Please try again."));
|
||||
}, timeoutS * 1000);
|
||||
|
||||
server.on("close", () => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
12
src/util/general.ts
Normal file
12
src/util/general.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { apiClient } from "../core/api-client";
|
||||
import { logError, logInfo } from "./logger";
|
||||
|
||||
export async function getUserId(username: string): Promise<string | null> {
|
||||
const user = await apiClient.users.getUserByName(username);
|
||||
if (user) {
|
||||
logInfo(`${user.name} => ${user.id}`);
|
||||
return user.id;
|
||||
}
|
||||
logError(`no user with name ${username} found`);
|
||||
return null;
|
||||
}
|
||||
@@ -4,6 +4,10 @@ export function logError(...args: unknown[]) {
|
||||
console.log(chalk.red(args));
|
||||
}
|
||||
|
||||
export function logWarning(...args: unknown[]) {
|
||||
console.log(chalk.yellow(args));
|
||||
}
|
||||
|
||||
export function logSuccess(...args: unknown[]) {
|
||||
console.log(chalk.green(args));
|
||||
}
|
||||
Reference in New Issue
Block a user