tested & working basic bot
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
package-lock.jsom
|
package-lock.jsom
|
||||||
|
db
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"build": "npm run clean && tsc -p .",
|
"build": "npm run clean && tsc -p .",
|
||||||
"start": "node dist/bot.js",
|
"start": "node dist/bot.js",
|
||||||
"dev": "nodemon src/bot.ts --verbose",
|
"dev": "nodemon src/bot.ts",
|
||||||
"setup:env": "nodemon src/setup-env.ts --verbose",
|
"setup:env": "nodemon src/setup-env.ts",
|
||||||
"setup:auth": "nodemon src/setup-auth.ts --verbose"
|
"setup:auth": "nodemon src/setup-auth.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ChatUser } from "@twurple/chat";
|
||||||
import { chatClient } from "../core/chat-client.ts";
|
import { chatClient } from "../core/chat-client.ts";
|
||||||
import type { ICommand, ICommandRequirements } from "./interface.ts";
|
import type { ICommand, ICommandRequirements } from "./interface.ts";
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export abstract class BaseCommand implements ICommand {
|
|||||||
abstract name: string;
|
abstract name: string;
|
||||||
abstract cooldown: number;
|
abstract cooldown: number;
|
||||||
abstract enabled: boolean;
|
abstract enabled: boolean;
|
||||||
|
abstract checkPerms(user: ChatUser): boolean;
|
||||||
abstract triggered(...args: unknown[]): Promise<unknown>;
|
abstract triggered(...args: unknown[]): Promise<unknown>;
|
||||||
abstract requirements: ICommandRequirements;
|
abstract requirements: ICommandRequirements;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Collection } from "@discordjs/collection";
|
import { Collection } from "@discordjs/collection";
|
||||||
import { SongCommand } from "./command-song.ts";
|
import { SongCommand } from "./impl/song.ts";
|
||||||
import type { ICommand } from "./interface.ts";
|
import type { ICommand } from "./interface.ts";
|
||||||
|
|
||||||
export const commands = new Collection<string, ICommand>();
|
export const commands = new Collection<string, ICommand>();
|
||||||
|
|
||||||
commands.set(SongCommand.name, new SongCommand());
|
const songCommand = new SongCommand();
|
||||||
|
|
||||||
|
commands.set(songCommand.name, songCommand);
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import type { ChatMessage } from "@twurple/chat";
|
|
||||||
import axios from "axios";
|
|
||||||
import { Config } from "../config/config.ts";
|
|
||||||
import { logError, logSuccess } from "../util/logger.ts";
|
|
||||||
import { BaseCommand } from "./base-command.ts";
|
|
||||||
import type { ICommandRequirements } from "./interface.ts";
|
|
||||||
|
|
||||||
interface ISong {
|
|
||||||
title: string;
|
|
||||||
artists: string;
|
|
||||||
album: string;
|
|
||||||
playingFrom: string;
|
|
||||||
status: "playing" | "paused";
|
|
||||||
url: string;
|
|
||||||
current: string;
|
|
||||||
duration: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SongCommand extends BaseCommand {
|
|
||||||
name = "song";
|
|
||||||
cooldown = 0;
|
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
requirements: ICommandRequirements = {
|
|
||||||
developer: true,
|
|
||||||
mod: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
triggered = async (
|
|
||||||
channel: string,
|
|
||||||
user: string,
|
|
||||||
_text: string,
|
|
||||||
_msg: ChatMessage,
|
|
||||||
) => {
|
|
||||||
logSuccess(`${channel} ${user} song command triggered`);
|
|
||||||
logSuccess(await getSongFromTidal());
|
|
||||||
// client.say(channel, "testing");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSongFromTidal() {
|
|
||||||
axios.get<ISong>(`${Config.tidal.host}:${Config.tidal.port}/current`).then(
|
|
||||||
(response) => {
|
|
||||||
const currentSong = response.data;
|
|
||||||
|
|
||||||
const status = currentSong.status === "playing" ? "▶️" : "⏸️";
|
|
||||||
|
|
||||||
return `listening to ${currentSong.title} by ${currentSong.artists}. ${status} ${currentSong.current}/${currentSong.duration}. link: ${currentSong.url}`;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
logError("error getting song from tidal");
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
78
src/commands/impl/song.ts
Normal file
78
src/commands/impl/song.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { ChatMessage, ChatUser } from "@twurple/chat";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Config } from "../../config/config.ts";
|
||||||
|
import { chatClient } from "../../core/chat-client.ts";
|
||||||
|
import { logSuccess, logWarning } from "../../util/logger.ts";
|
||||||
|
import { BaseCommand } from "../base-command.ts";
|
||||||
|
import type { ICommandRequirements } from "../interface.ts";
|
||||||
|
|
||||||
|
interface ISong {
|
||||||
|
title: string;
|
||||||
|
artists: string;
|
||||||
|
album: string;
|
||||||
|
playingFrom: string;
|
||||||
|
status: "playing" | "paused";
|
||||||
|
url: string;
|
||||||
|
current: string;
|
||||||
|
duration: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SongCommand extends BaseCommand {
|
||||||
|
name = "song";
|
||||||
|
cooldown = 0;
|
||||||
|
enabled = true;
|
||||||
|
|
||||||
|
requirements: ICommandRequirements = {
|
||||||
|
developer: true,
|
||||||
|
mod: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
triggered = async (
|
||||||
|
channel: string,
|
||||||
|
user: string,
|
||||||
|
_text: string,
|
||||||
|
msg: ChatMessage,
|
||||||
|
) => {
|
||||||
|
logSuccess(`${channel} ${user} song command triggered`);
|
||||||
|
const song = await getSongFromTidal();
|
||||||
|
if (song) {
|
||||||
|
logSuccess(song);
|
||||||
|
chatClient.say(channel, song, { replyTo: msg });
|
||||||
|
} else {
|
||||||
|
chatClient.say(channel, "tidal not running..", { replyTo: msg });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPerms = (user: ChatUser): boolean => {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this.requirements.developer &&
|
||||||
|
!Config.developers.includes(user.userId)) ||
|
||||||
|
(this.requirements.mod && !user.isMod)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSongFromTidal(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<ISong>(
|
||||||
|
`${Config.tidal.host}:${Config.tidal.port}/current`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSong = response.data;
|
||||||
|
|
||||||
|
const status = currentSong.status === "playing" ? "▶️" : "⏸️";
|
||||||
|
|
||||||
|
return `listening to ${currentSong.title} by ${currentSong.artists}. ${status} ${currentSong.current}/${currentSong.duration}. link: ${currentSong.url}`;
|
||||||
|
} catch {
|
||||||
|
logWarning("error getting song from tidal");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import type { ChatUser } from "@twurple/chat";
|
||||||
|
|
||||||
export interface ICommand {
|
export interface ICommand {
|
||||||
name: string;
|
name: string;
|
||||||
cooldown: number;
|
cooldown: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
checkPerms(user: ChatUser): boolean;
|
||||||
triggered(...args: unknown[]): Promise<unknown>;
|
triggered(...args: unknown[]): Promise<unknown>;
|
||||||
requirements: ICommandRequirements;
|
requirements: ICommandRequirements;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import type { AccessToken } from "@twurple/auth";
|
import type { AccessToken } from "@twurple/auth";
|
||||||
|
import { logError, logInfo } from "../util/logger.ts";
|
||||||
|
|
||||||
export class TokenManager {
|
export class TokenManager {
|
||||||
private readonly tokenFilePath: string;
|
private readonly tokenFilePath: string;
|
||||||
|
|
||||||
constructor(userId: string) {
|
constructor(userId: string) {
|
||||||
this.tokenFilePath = `../db/token.${userId}.json`;
|
this.tokenFilePath = `./db/token.${userId}.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(): Promise<AccessToken | null> {
|
async getToken(): Promise<AccessToken | null> {
|
||||||
try {
|
try {
|
||||||
const tokenFile = await fs.readFile(this.tokenFilePath, "utf-8");
|
const tokenFile = await fs.readFile(this.tokenFilePath, "utf-8");
|
||||||
|
logInfo(`token file ${this.tokenFilePath} read`);
|
||||||
return JSON.parse(tokenFile);
|
return JSON.parse(tokenFile);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.log(
|
logError(
|
||||||
`token file ${this.tokenFilePath} not found. please run the setup`,
|
`token file ${this.tokenFilePath} not found. please run the setup`,
|
||||||
);
|
);
|
||||||
return null;
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTokenFile(tokenData: AccessToken): Promise<void> {
|
async createTokenFile(tokenData: AccessToken): Promise<void> {
|
||||||
const formattedTokens = JSON.stringify(tokenData, null, 4);
|
const formattedToken = JSON.stringify(tokenData, null, 4);
|
||||||
await fs.writeFile(this.tokenFilePath, formattedTokens, "utf-8");
|
await fs.writeFile(this.tokenFilePath, formattedToken, "utf-8");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { logSuccess } from "../util/logger.ts";
|
import { logSuccess } from "../../util/logger.ts";
|
||||||
import { BaseEvent } from "./base-event.ts";
|
import { BaseEvent } from "../base-event.ts";
|
||||||
import type { EventName } from "./registry.ts";
|
import type { EventName } from "../registry.ts";
|
||||||
|
|
||||||
export default class ConnectedEvent extends BaseEvent {
|
export default class ConnectedEvent extends BaseEvent {
|
||||||
name: EventName = "connected";
|
name: EventName = "connected";
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Collection } from "@discordjs/collection";
|
import { Collection } from "@discordjs/collection";
|
||||||
import type { ChatMessage } from "@twurple/chat";
|
import type { ChatMessage } from "@twurple/chat";
|
||||||
import { commands } from "../commands/collection.ts";
|
import { commands } from "../../commands/collection.ts";
|
||||||
import type { ICommand } from "../commands/interface.ts";
|
import type { ICommand } from "../../commands/interface.ts";
|
||||||
import { Config } from "../config/config.ts";
|
import { Config } from "../../config/config.ts";
|
||||||
import { logInfo } from "../util/logger.ts";
|
import { BaseEvent } from "../base-event.ts";
|
||||||
import { BaseEvent } from "./base-event.ts";
|
import type { EventName } from "../registry.ts";
|
||||||
import type { EventName } from "./registry.ts";
|
|
||||||
|
|
||||||
const Cooldowns = new Collection<string, number>();
|
const Cooldowns = new Collection<string, number>();
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ async function checkMessage(
|
|||||||
text: string,
|
text: string,
|
||||||
msg: ChatMessage,
|
msg: ChatMessage,
|
||||||
) {
|
) {
|
||||||
logInfo(`message seen: ${channel} - ${user} - ${text}`);
|
// logInfo(`message seen: ${channel} - ${user} - ${text}`);
|
||||||
const prefix = Config.prefix;
|
const prefix = Config.prefix;
|
||||||
if (!text.startsWith(prefix)) return;
|
if (!text.startsWith(prefix)) return;
|
||||||
|
|
||||||
@@ -37,13 +36,11 @@ async function checkMessage(
|
|||||||
.trim()
|
.trim()
|
||||||
.split(/ +/g)[0]
|
.split(/ +/g)[0]
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
// logInfo(`available commands: ${commands.toJSON()}`);
|
||||||
|
// logInfo(`searching for command: ${commandName}`);
|
||||||
const command = commands.get(commandName);
|
const command = commands.get(commandName);
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
if (!command.enabled) return;
|
if (!command.checkPerms) return;
|
||||||
|
|
||||||
if (command.requirements.developer && !isDeveloper(msg.userInfo.userId))
|
|
||||||
return;
|
|
||||||
if (command.requirements.mod && !msg.userInfo.isMod) return;
|
|
||||||
|
|
||||||
const timeLeft = checkCooldown(command);
|
const timeLeft = checkCooldown(command);
|
||||||
if (timeLeft > 0) {
|
if (timeLeft > 0) {
|
||||||
@@ -56,17 +53,13 @@ async function checkMessage(
|
|||||||
await command.triggered(channel, user, text, msg);
|
await command.triggered(channel, user, text, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeveloper(userId: string): boolean {
|
|
||||||
return Config.developers.includes(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCooldown(command: ICommand): number {
|
function checkCooldown(command: ICommand): number {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (command.cooldown > 0) {
|
if (command.cooldown > 0) {
|
||||||
const cooldownTime = Cooldowns.get(command.name);
|
const cooldownTime = Cooldowns.get(command.name);
|
||||||
if (cooldownTime) {
|
if (cooldownTime) {
|
||||||
if (cooldownTime < now) {
|
if (cooldownTime < now) {
|
||||||
const timeLeft = 0; // TODO
|
const timeLeft = 0; // TODO!!!
|
||||||
return timeLeft;
|
return timeLeft;
|
||||||
} else {
|
} else {
|
||||||
Cooldowns.set(command.name, now + command.cooldown * 1000);
|
Cooldowns.set(command.name, now + command.cooldown * 1000);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { chatClient } from "../core/chat-client.ts";
|
import { chatClient } from "../core/chat-client.ts";
|
||||||
import ConnectedEvent from "./event-connected.ts";
|
import { logInfo } from "../util/logger.ts";
|
||||||
import MessageEvent from "./event-message.ts";
|
import ConnectedEvent from "./impl/connected.ts";
|
||||||
|
import MessageEvent from "./impl/message.ts";
|
||||||
import type { IEvent } from "./interface.ts";
|
import type { IEvent } from "./interface.ts";
|
||||||
|
|
||||||
export const eventRegistry = {
|
export const eventRegistry = {
|
||||||
@@ -13,6 +14,7 @@ export function registerAllEvents() {
|
|||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
eventRegistry[event.name](event); // Registers the event
|
eventRegistry[event.name](event); // Registers the event
|
||||||
|
logInfo(`event ${event.name} registered`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
import { exchangeCode } from "@twurple/auth";
|
import { exchangeCode } from "@twurple/auth";
|
||||||
import { Config } from "./config/config";
|
import { Config } from "./config/config.ts";
|
||||||
import { TokenManager } from "./core/token-manager";
|
import { TokenManager } from "./core/token-manager.ts";
|
||||||
import { TwitchAuth } from "./util/auth";
|
import { TwitchAuth } from "./util/auth.ts";
|
||||||
import { getUserId, promptForInput } from "./util/general";
|
import { logInfo } from "./util/logger.ts";
|
||||||
import { logError, logInfo } from "./util/logger";
|
|
||||||
|
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
const redirectUri = `http://localhost:${port}`;
|
const redirectUri = `http://localhost:${port}`;
|
||||||
const scopes = ["chat:read", "chat:edit", "channel:moderate"];
|
const scopes = ["chat:read", "chat:edit", "channel:moderate"];
|
||||||
|
|
||||||
const userName = await promptForInput("enter userName to authorize on: ");
|
|
||||||
|
|
||||||
const userId = await getUserId(userName);
|
|
||||||
if (!userId) {
|
|
||||||
logError("user not found. please check the configuration");
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth = new TwitchAuth(redirectUri);
|
const auth = new TwitchAuth(redirectUri);
|
||||||
const authUrl = auth.getAuthorizationUrl(scopes);
|
const state = auth.generateState();
|
||||||
|
const authUrl = auth.getAuthorizationUrl(scopes, state);
|
||||||
logInfo("To authorize your Twitch bot, visit this URL:");
|
logInfo("To authorize your Twitch bot, visit this URL:");
|
||||||
logInfo(authUrl);
|
logInfo(authUrl);
|
||||||
const code = await auth.startCallbackServer(port, 120);
|
const code = await auth.startCallbackServer(port, 120, state);
|
||||||
|
|
||||||
const tokenManager = new TokenManager(userId);
|
const tokenManager = new TokenManager(Config.bot_user_id);
|
||||||
|
|
||||||
tokenManager.createTokenFile(
|
tokenManager.createTokenFile(
|
||||||
await exchangeCode(Config.client_id, Config.client_secret, code, redirectUri),
|
await exchangeCode(Config.client_id, Config.client_secret, code, redirectUri),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getUserId, promptForInput } from "./util/general.ts";
|
import { getUserId, promptForInput } from "./util/general.ts";
|
||||||
import { logError, logInfo, logWarning } from "./util/logger.ts";
|
import { logError, logSuccess, logWarning } from "./util/logger.ts";
|
||||||
|
|
||||||
const botname = await promptForInput("enter bot username: ");
|
const botname = await promptForInput("enter bot username: ");
|
||||||
const developers = (
|
const developers = (
|
||||||
@@ -13,7 +13,7 @@ if (!botId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const developerIds = [];
|
const developerIds = [];
|
||||||
for (const dev in developers) {
|
for (const dev of developers) {
|
||||||
const devId = await getUserId(dev);
|
const devId = await getUserId(dev);
|
||||||
if (devId) {
|
if (devId) {
|
||||||
developerIds.push(devId);
|
developerIds.push(devId);
|
||||||
@@ -22,7 +22,7 @@ for (const dev in developers) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`Userid of bot '${botname}' => '${botId}'`);
|
logSuccess(`Userid of bot '${botname}' => '${botId}'`);
|
||||||
logInfo(
|
logSuccess(
|
||||||
`Userids of developers '${developers.join(",")}' => '${developerIds.join(",")}'`,
|
`Userids of developers '${developers.join(",")}' => '${developerIds.join(",")}'`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as crypto from "node:crypto";
|
||||||
import {
|
import {
|
||||||
createServer,
|
createServer,
|
||||||
type IncomingMessage,
|
type IncomingMessage,
|
||||||
@@ -5,6 +6,7 @@ import {
|
|||||||
} from "node:http";
|
} from "node:http";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import { Config } from "../config/config.ts";
|
import { Config } from "../config/config.ts";
|
||||||
|
import { logError, logInfo } from "./logger.ts";
|
||||||
|
|
||||||
export class TwitchAuth {
|
export class TwitchAuth {
|
||||||
private readonly redirectUri: string;
|
private readonly redirectUri: string;
|
||||||
@@ -13,20 +15,29 @@ export class TwitchAuth {
|
|||||||
this.redirectUri = redirectUri;
|
this.redirectUri = redirectUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthorizationUrl(scopes: string[]): string {
|
getAuthorizationUrl(scopes: string[], state: string): string {
|
||||||
const baseUrl = "https://id.twitch.tv/oauth2/authorize";
|
const baseUrl = "https://id.twitch.tv/oauth2/authorize";
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
client_id: Config.client_id,
|
client_id: Config.client_id,
|
||||||
redirect_uri: this.redirectUri,
|
redirect_uri: this.redirectUri,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
|
state: state,
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${baseUrl}?${params.toString()}`;
|
return `${baseUrl}?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
startCallbackServer(port: number, timeoutS: number = 120): Promise<string> {
|
generateState(): string {
|
||||||
return new Promise((resolve, reject) => {
|
return crypto.randomBytes(20).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
startCallbackServer(
|
||||||
|
port: number,
|
||||||
|
timeoutS: number = 120,
|
||||||
|
state: string,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
const server = createServer(
|
const server = createServer(
|
||||||
(req: IncomingMessage, res: ServerResponse) => {
|
(req: IncomingMessage, res: ServerResponse) => {
|
||||||
if (!req.url) {
|
if (!req.url) {
|
||||||
@@ -38,8 +49,15 @@ export class TwitchAuth {
|
|||||||
const url = new URL(req.url, `http://localhost:${port}`);
|
const url = new URL(req.url, `http://localhost:${port}`);
|
||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const error = url.searchParams.get("error");
|
const error = url.searchParams.get("error");
|
||||||
|
const responseState = url.searchParams.get("state");
|
||||||
const errorDescription = url.searchParams.get("error_description");
|
const errorDescription = url.searchParams.get("error_description");
|
||||||
|
|
||||||
|
if (state !== responseState) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/html" });
|
||||||
|
res.end("<h1>Wrong state</h1><p>Invalid state param</p>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
res.writeHead(400, { "Content-Type": "text/html" });
|
res.writeHead(400, { "Content-Type": "text/html" });
|
||||||
res.end(`
|
res.end(`
|
||||||
@@ -49,10 +67,8 @@ export class TwitchAuth {
|
|||||||
<p>You can close this window.</p>
|
<p>You can close this window.</p>
|
||||||
`);
|
`);
|
||||||
server.close();
|
server.close();
|
||||||
reject(
|
logError(`authorization failed: ${error} - ${errorDescription}`);
|
||||||
new Error(`Authorization failed: ${error} - ${errorDescription}`),
|
throw new Error();
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -73,18 +89,20 @@ export class TwitchAuth {
|
|||||||
);
|
);
|
||||||
|
|
||||||
server.listen(port, "localhost", () => {
|
server.listen(port, "localhost", () => {
|
||||||
console.log(
|
logInfo(
|
||||||
`Waiting for authorization callback on http://localhost:${port}`,
|
`waiting for authorization callback on http://localhost:${port}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
reject(new Error(`Server error: ${err.message}`));
|
logError(`server error: ${err.message}`);
|
||||||
|
throw new Error();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
server.close();
|
server.close();
|
||||||
reject(new Error("Authorization timeout. Please try again."));
|
logError("authorization timeout");
|
||||||
|
throw new Error();
|
||||||
}, timeoutS * 1000);
|
}, timeoutS * 1000);
|
||||||
|
|
||||||
server.on("close", () => {
|
server.on("close", () => {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import * as readline from "node:readline";
|
import * as readline from "node:readline";
|
||||||
import { apiClient } from "../core/api-client.ts";
|
import { apiClient } from "../core/api-client.ts";
|
||||||
import { logError, logInfo } from "./logger.ts";
|
import { logSuccess, logWarning } from "./logger.ts";
|
||||||
|
|
||||||
export async function getUserId(username: string): Promise<string | null> {
|
export async function getUserId(username: string): Promise<string | null> {
|
||||||
const user = await apiClient.users.getUserByName(username);
|
const user = await apiClient.users.getUserByName(username);
|
||||||
if (user) {
|
if (user?.id) {
|
||||||
logInfo(`${user.name} => ${user.id}`);
|
logSuccess(`${user.name} => ${user.id}`);
|
||||||
return user.id;
|
return user.id;
|
||||||
}
|
}
|
||||||
logError(`no user with name ${username} found`);
|
logWarning(`no user with name ${username} found`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
const verbose = process.argv.includes("--verbose");
|
|
||||||
|
|
||||||
export function logError(...args: unknown[]) {
|
export function logError(...args: unknown[]) {
|
||||||
console.log(chalk.red(args));
|
console.log(chalk.red(args));
|
||||||
}
|
}
|
||||||
@@ -15,7 +13,5 @@ export function logSuccess(...args: unknown[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function logInfo(...args: unknown[]) {
|
export function logInfo(...args: unknown[]) {
|
||||||
if (verbose) {
|
|
||||||
console.log(chalk.cyan(args));
|
console.log(chalk.cyan(args));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user