add pretty console formatting

This commit is contained in:
2025-07-04 18:19:25 +02:00
parent c0fc8bccf2
commit 2fd30bd87e
13 changed files with 60 additions and 37 deletions

View File

@@ -1,6 +1,9 @@
import { RefreshingAuthProvider, exchangeCode, type AccessToken } from "@twurple/auth"; import { RefreshingAuthProvider, exchangeCode, type AccessToken } from "@twurple/auth";
import { createAuthRecord, deleteAuthRecord, getAuthRecord, updateAuthRecord } from "./db/dbAuth"; import { createAuthRecord, deleteAuthRecord, getAuthRecord, updateAuthRecord } from "./db/dbAuth";
import { logger } from ".";
import kleur from "kleur";
async function initAuth(userId: string, clientId: string, clientSecret: string, requestedIntents: string[], streamer: boolean): Promise<AccessToken> { async function initAuth(userId: string, clientId: string, clientSecret: string, requestedIntents: string[], streamer: boolean): Promise<AccessToken> {
const port = process.env.REDIRECT_PORT ?? 3456 const port = process.env.REDIRECT_PORT ?? 3456
const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`; const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`;
@@ -9,9 +12,9 @@ async function initAuth(userId: string, clientId: string, clientSecret: string,
const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase(); const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase();
// Generate random state variable to prevent cross-site-scripting attacks // Generate random state variable to prevent cross-site-scripting attacks
const instruction = `Visit this URL as ${streamer ? 'the streamer' : 'the chatter'} to authenticate the bot.` const instruction = `Visit this URL as ${kleur.red().underline().italic(streamer ? 'the streamer' : 'the chatter')} to authenticate the bot.`
console.info(instruction); logger.info(instruction);
console.info(`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join('+')}&state=${state}`); logger.info(`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join('+')}&state=${state}`);
const createCodePromise = () => { const createCodePromise = () => {
let resolver: (code: string) => void; let resolver: (code: string) => void;
@@ -39,20 +42,20 @@ async function initAuth(userId: string, clientId: string, clientSecret: string,
const code = await codepromise; const code = await codepromise;
server.stop(false); server.stop(false);
console.info(`Authentication code received.`); logger.info(`Authentication code received.`);
const tokenData = await exchangeCode(clientId, clientSecret, code, redirectURL); const tokenData = await exchangeCode(clientId, clientSecret, code, redirectURL);
console.info(`Successfully authenticated code.`); logger.info(`Successfully authenticated code.`);
await createAuthRecord(tokenData, userId); await createAuthRecord(tokenData, userId);
console.info(`Successfully saved auth data in the database.`); logger.ok(`Successfully saved auth data in the database.`);
return tokenData; return tokenData;
}; };
export async function createAuthProvider(user: string, intents: string[], streamer = false): Promise<RefreshingAuthProvider> { export async function createAuthProvider(user: string, intents: string[], streamer = false): Promise<RefreshingAuthProvider> {
const clientId = process.env.CLIENT_ID; const clientId = process.env.CLIENT_ID;
if (!clientId) { console.error("Please provide a CLIENT_ID in .env."); process.exit(1); }; if (!clientId) { logger.enverr("CLIENT_ID"); process.exit(1); };
const clientSecret = process.env.CLIENT_SECRET; const clientSecret = process.env.CLIENT_SECRET;
if (!clientSecret) { console.error("Please provide a CLIENT_SECRET in .env."); process.exit(1); }; if (!clientSecret) { logger.enverr("CLIENT_SECRET"); process.exit(1); };
const authRecord = await getAuthRecord(user, intents); const authRecord = await getAuthRecord(user, intents);
@@ -65,18 +68,18 @@ export async function createAuthProvider(user: string, intents: string[], stream
await authData.addUserForToken(token, intents); await authData.addUserForToken(token, intents);
authData.onRefresh(async (user, token) => { authData.onRefresh(async (user, token) => {
console.info(`Successfully refreshed auth for user ${user}`); logger.ok(`Successfully refreshed auth for user ${user}`);
await updateAuthRecord(user, token); await updateAuthRecord(user, token);
}); });
authData.onRefreshFailure((user, err) => { authData.onRefreshFailure((user, err) => {
console.error(`ERROR: Failed to refresh auth for user ${user}: ${err.name} ${err.message}`); logger.err(`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`);
}); });
try { try {
await authData.refreshAccessTokenForUser(user); await authData.refreshAccessTokenForUser(user);
} catch (err) { } catch (err) {
console.error(`Failed to refresh user ${user}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`); logger.err(`Failed to refresh user ${user}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`);
await deleteAuthRecord(user); await deleteAuthRecord(user);
}; };

View File

@@ -4,6 +4,7 @@ import { getUserRecord } from "../db/dbUser";
import parseCommandArgs from "../lib/parseCommandArgs"; import parseCommandArgs from "../lib/parseCommandArgs";
import { changeBalance } from "../lib/changeBalance"; import { changeBalance } from "../lib/changeBalance";
import { User } from "../user"; import { User } from "../user";
import { logger } from "..";
export default new Command('donate', ['donate'], 'chatter', async (msg, user) => { export default new Command('donate', ['donate'], 'chatter', async (msg, user) => {
const args = parseCommandArgs(msg.messageText); const args = parseCommandArgs(msg.messageText);
@@ -37,7 +38,7 @@ export default new Command('donate', ['donate'], 'chatter', async (msg, user) =>
} else { } else {
// TODO: Rewrite this section // TODO: Rewrite this section
await sendMessage(`Failed to give ${target.displayName} ${amount} qbuck${(amount === 1 ? '' : 's')}`, msg.messageId); await sendMessage(`Failed to give ${target.displayName} ${amount} qbuck${(amount === 1 ? '' : 's')}`, msg.messageId);
console.error(`WARNING: Qweribucks donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`); logger.err(`WARNING: Qweribucks donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`);
}; };
await Promise.all([ await Promise.all([

View File

@@ -1,4 +1,5 @@
import { Command, sendMessage } from "."; import { Command, sendMessage } from ".";
import { logger } from "..";
import type { userRecord } from "../db/connection"; import type { userRecord } from "../db/connection";
import { getUserRecord } from "../db/dbUser"; import { getUserRecord } from "../db/dbUser";
import items, { changeItemCount } from "../items"; import items, { changeItemCount } from "../items";
@@ -41,7 +42,7 @@ export default new Command('give', ['give'], 'chatter', async (msg, user) => {
} else { } else {
// TODO: Rewrite this section // TODO: Rewrite this section
await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId); await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId);
console.error(`WARNING: Item donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`); logger.warn(`WARNING: Item donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`);
}; };
await user.clearLock(); await user.clearLock();
await target.clearLock(); await target.clearLock();

View File

@@ -1,9 +1,10 @@
import type { AccessToken } from "@twurple/auth"; import type { AccessToken } from "@twurple/auth";
import PocketBase, { RecordService } from "pocketbase"; import PocketBase, { RecordService } from "pocketbase";
import type { inventory } from "../items"; import type { inventory } from "../items";
import { logger } from "..";
const pocketbaseurl = process.env.POCKETBASE_URL ?? "localhost:8090"; const pocketbaseurl = process.env.POCKETBASE_URL ?? "localhost:8090";
if (pocketbaseurl === "") { console.error("Please provide a POCKETBASE_URL in .env."); process.exit(1); }; if (pocketbaseurl === "") { logger.enverr("POCKETBASE_URL"); process.exit(1); };
export type authRecord = { export type authRecord = {
id: string; id: string;

View File

@@ -1,12 +1,13 @@
import pocketbase from "./connection"; import pocketbase from "./connection";
import { User } from "../user"; import { User } from "../user";
import { logger } from "..";
const pb = pocketbase.collection('timeouts'); const pb = pocketbase.collection('timeouts');
export async function createTimeoutRecord(user: User, target: User, item: string): Promise<void> { export async function createTimeoutRecord(user: User, target: User, item: string): Promise<void> {
try { try {
await pb.create({ user: user.id, target: target.id, item }); await pb.create({ user: user.id, target: target.id, item });
} catch (err) { } catch (err) {
console.error(`Failed to create timeout record in database: user: ${user.id}, target: ${target.id}, item: ${item}`); logger.err(`Failed to create timeout record in database: user: ${user.id}, target: ${target.id}, item: ${item}`);
console.error(err); logger.err(err as string);
}; };
}; };

View File

@@ -1,12 +1,13 @@
import pocketbase from "./connection"; import pocketbase from "./connection";
import { User } from "../user"; import { User } from "../user";
import { logger } from "..";
const pb = pocketbase.collection('usedItems'); const pb = pocketbase.collection('usedItems');
export async function createUsedItemRecord(user: User, item: string): Promise<void> { export async function createUsedItemRecord(user: User, item: string): Promise<void> {
try { try {
await pb.create({ user: user.id, item }); await pb.create({ user: user.id, item });
} catch (err) { } catch (err) {
console.error(`Failed to create usedItem record in database: user: ${user.id}, item: ${item}`); logger.err(`Failed to create usedItem record in database: user: ${user.id}, item: ${item}`);
console.error(err); logger.err(err as string);
}; };
}; };

View File

@@ -1,6 +1,7 @@
import pocketbase, { type userRecord } from "./connection"; import pocketbase, { type userRecord } from "./connection";
import { emptyInventory, itemarray } from "../items"; import { emptyInventory, itemarray } from "../items";
import type { User } from "../user"; import type { User } from "../user";
import { logger } from "..";
const pb = pocketbase.collection('users'); const pb = pocketbase.collection('users');
/** Use this function to both ensure existance and to retreive data */ /** Use this function to both ensure existance and to retreive data */
@@ -38,7 +39,7 @@ export async function updateUserRecord(user: User, newData: userRecord): Promise
await pb.update(user.id, newData); await pb.update(user.id, newData);
return true; return true;
} catch (err) { } catch (err) {
console.error(err); logger.err(err as string);
return false; return false;
}; };
}; };

View File

@@ -1,24 +1,25 @@
import { eventSub, streamerApi, streamerId } from ".."; import kleur from "kleur";
import { eventSub, streamerApi, streamerId, logger } from "..";
eventSub.onRevoke(event => { eventSub.onRevoke(event => {
console.info(`Successfully revoked EventSub subscription: ${event.id}`); logger.ok(`Successfully revoked EventSub subscription: ${event.id}`);
}); });
eventSub.onSubscriptionCreateSuccess(event => { eventSub.onSubscriptionCreateSuccess(event => {
console.info(`Successfully created EventSub subscription: ${event.id}`); logger.ok(`Successfully created EventSub subscription: ${kleur.underline(event.id)}`);
deleteDuplicateSubscriptions.refresh(); deleteDuplicateSubscriptions.refresh();
}); });
eventSub.onSubscriptionCreateFailure(event => { eventSub.onSubscriptionCreateFailure(event => {
console.error(`Failed to create EventSub subscription: ${event.id}`); logger.err(`Failed to create EventSub subscription: ${event.id}`);
}); });
eventSub.onSubscriptionDeleteSuccess(event => { eventSub.onSubscriptionDeleteSuccess(event => {
console.info(`Successfully deleted EventSub subscription: ${event.id}`); logger.ok(`Successfully deleted EventSub subscription: ${event.id}`);
}); });
eventSub.onSubscriptionDeleteFailure(event => { eventSub.onSubscriptionDeleteFailure(event => {
console.error(`Failed to delete EventSub subscription: ${event.id}`); logger.err(`Failed to delete EventSub subscription: ${event.id}`);
}); });
import { readdir } from 'node:fs/promises'; import { readdir } from 'node:fs/promises';
@@ -36,12 +37,12 @@ import { StaticAuthProvider } from "@twurple/auth";
import { ApiClient, HelixEventSubSubscription } from "@twurple/api"; import { ApiClient, HelixEventSubSubscription } from "@twurple/api";
const deleteDuplicateSubscriptions = setTimeout(async () => { const deleteDuplicateSubscriptions = setTimeout(async () => {
console.info('Deleting all double subscriptions'); logger.info('Deleting all double subscriptions');
const tokendata = await streamerApi.getTokenInfo(); const tokendata = await streamerApi.getTokenInfo();
const authdata = await getAuthRecord(streamerId, []); const authdata = await getAuthRecord(streamerId, []);
const tempauth = new StaticAuthProvider(tokendata.clientId, authdata?.accesstoken.accessToken!); const tempauth = new StaticAuthProvider(tokendata.clientId, authdata?.accesstoken.accessToken!);
const tempapi: ApiClient = new ApiClient({ authProvider: tempauth }); const tempapi: ApiClient = new ApiClient({ authProvider: tempauth });
console.info('Created the temporary API client'); logger.info('Created the temporary API client');
const subs = await tempapi.eventSub.getSubscriptionsForStatus('enabled'); const subs = await tempapi.eventSub.getSubscriptionsForStatus('enabled');
const seen = new Map(); const seen = new Map();
const duplicates: HelixEventSubSubscription[] = []; const duplicates: HelixEventSubSubscription[] = [];
@@ -58,8 +59,8 @@ const deleteDuplicateSubscriptions = setTimeout(async () => {
for (const sub of duplicates) { for (const sub of duplicates) {
await tempapi.eventSub.deleteSubscription(sub.id); await tempapi.eventSub.deleteSubscription(sub.id);
console.info(`Deleted sub: id: ${sub.id}, type: ${sub.type}`); logger.ok(`Deleted sub: id: ${sub.id}, type: ${sub.type}`);
}; };
if (duplicates.length === 0) console.info('No duplicate subscriptions found'); if (duplicates.length === 0) logger.ok('No duplicate subscriptions found');
console.info('Removed temporary API client'); else logger.ok('Deleted all duplicate EventSub subscriptions');
}, 5000); }, 5000);

View File

@@ -1,10 +1,10 @@
import { chatterId, streamerId, eventSub, commandPrefix, singleUserMode, streamerUsers } from ".."; import { chatterId, streamerId, eventSub, commandPrefix, singleUserMode, streamerUsers, logger } from "..";
import { User } from "../user"; import { User } from "../user";
import commands, { sendMessage } from "../commands"; import commands, { sendMessage } from "../commands";
import { redis } from "bun"; import { redis } from "bun";
import { isAdmin } from "../lib/admins"; import { isAdmin } from "../lib/admins";
console.info(`Loaded the following commands: ${commands.keys().toArray().join(', ')}`); logger.info(`Loaded the following commands: ${commands.keys().toArray().join(', ')}`);
eventSub.onChannelChatMessage(streamerId, streamerId, async msg => { eventSub.onChannelChatMessage(streamerId, streamerId, async msg => {
// return if double user mode is on and the chatter says something, we don't need them // return if double user mode is on and the chatter says something, we don't need them
@@ -43,7 +43,7 @@ eventSub.onChannelChatMessage(streamerId, streamerId, async msg => {
try { await selected.execute(msg, user!); } try { await selected.execute(msg, user!); }
catch (err) { catch (err) {
console.error(err); logger.err(err as string);
await sendMessage('ERROR: Something went wrong', msg.messageId); await sendMessage('ERROR: Something went wrong', msg.messageId);
await user?.clearLock(); await user?.clearLock();
}; };

View File

@@ -2,15 +2,24 @@ import { createAuthProvider } from "./auth";
import { ApiClient } from "@twurple/api"; import { ApiClient } from "@twurple/api";
import { EventSubWsListener } from "@twurple/eventsub-ws"; import { EventSubWsListener } from "@twurple/eventsub-ws";
import { addAdmin } from "./lib/admins"; import { addAdmin } from "./lib/admins";
import kleur from "kleur";
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"]; const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"];
const STREAMERINTENTS = ["user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:banned_users"]; const STREAMERINTENTS = ["user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:banned_users"];
export const logger = {
err: (arg: string) => console.error(kleur.red().bold().italic('[ERROR] ') + kleur.red().bold(arg)),
warn: (arg: string) => console.warn(kleur.yellow().bold().italic('[WARN] ') + kleur.yellow().bold(arg)),
info: (arg: string) => console.info(kleur.white().bold().italic('[INFO] ') + kleur.white(arg)),
ok: (arg: string) => console.info(kleur.green().bold(arg)),
enverr: (arg: string) => logger.err(`Please provide a ${arg} in the .env`)
};
export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true'; export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true';
export const chatterId = process.env.CHATTER_ID ?? ""; export const chatterId = process.env.CHATTER_ID ?? "";
if (chatterId === "") { console.error('Please set a CHATTER_ID in the .env'); process.exit(1); }; if (chatterId === "") { logger.enverr('CHATTER_ID'); process.exit(1); };
export const streamerId = process.env.STREAMER_ID ?? ""; export const streamerId = process.env.STREAMER_ID ?? "";
if (streamerId === "") { console.error('Please set a STREAMER_ID in the .env'); process.exit(1); }; if (streamerId === "") { logger.enverr('STREAMER_ID'); process.exit(1); };
export const chatterAuthProvider = await createAuthProvider(chatterId, singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : CHATTERINTENTS); export const chatterAuthProvider = await createAuthProvider(chatterId, singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : CHATTERINTENTS);
export const streamerAuthProvider = singleUserMode ? undefined : await createAuthProvider(streamerId, STREAMERINTENTS, true); export const streamerAuthProvider = singleUserMode ? undefined : await createAuthProvider(streamerId, STREAMERINTENTS, true);

View File

@@ -1,4 +1,4 @@
import { streamerApi, streamerId, streamerUsers } from ".."; import { logger, streamerApi, streamerId, streamerUsers } from "..";
import { User } from "../user"; import { User } from "../user";
type SuccessfulTimeout = { status: true }; type SuccessfulTimeout = { status: true };
@@ -25,7 +25,7 @@ export const timeout = async (user: User, reason: string, duration?: number): Pr
try { try {
await streamerApi.moderation.banUser(streamerId, { user: user.id, reason, duration }); await streamerApi.moderation.banUser(streamerId, { user: user.id, reason, duration });
} catch (err) { } catch (err) {
console.error(err); logger.err(err as string);
return { status: false, reason: 'unknown' } return { status: false, reason: 'unknown' }
}; };

View File

@@ -6,6 +6,7 @@
"dependencies": { "dependencies": {
"@twurple/auth": "^7.3.0", "@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0", "@twurple/eventsub-ws": "^7.3.0",
"kleur": "^4.1.5",
"pocketbase": "^0.26.1", "pocketbase": "^0.26.1",
}, },
"devDependencies": { "devDependencies": {
@@ -57,6 +58,8 @@
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@twurple/auth": "^7.3.0", "@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0", "@twurple/eventsub-ws": "^7.3.0",
"kleur": "^4.1.5",
"pocketbase": "^0.26.1" "pocketbase": "^0.26.1"
} }
} }