diff --git a/apis/websocket/config.json b/apis/websocket/config.json index 8d54b6c..0217e3a 100755 --- a/apis/websocket/config.json +++ b/apis/websocket/config.json @@ -5,5 +5,5 @@ "port": 3000, "ocrConcurrentQueues": 1, "clientHeartbeatInterval": 5000, - "debugLogsInProduction": false + "consoleLogLevel": "silly" } diff --git a/apis/websocket/config.schema.json b/apis/websocket/config.schema.json index 30fc6f7..42e1214 100755 --- a/apis/websocket/config.schema.json +++ b/apis/websocket/config.schema.json @@ -22,10 +22,11 @@ "type": "integer", "default": 60000 }, - "debugLogsInProduction": { - "description": "Whether to print debug logs in production", - "type": "boolean", - "default": false + "consoleLogLevel": { + "description": "The log level to print to console", + "type": "string", + "enum": ["error", "warn", "info", "verbose", "debug", "silly", "none"], + "default": "info" } } } diff --git a/apis/websocket/src/classes/Client.ts b/apis/websocket/src/classes/Client.ts index dcfdbc1..6de63d0 100755 --- a/apis/websocket/src/classes/Client.ts +++ b/apis/websocket/src/classes/Client.ts @@ -47,43 +47,29 @@ export default class Client { this.#emitter.emit('ready') }) .catch(() => { - if (this.disconnected === false) - this.disconnect(DisconnectReason.ServerError) + if (this.disconnected === false) this.disconnect(DisconnectReason.ServerError) else this.forceDisconnect(DisconnectReason.ServerError) }) } - on( - name: TOpName, - handler: ClientEventHandlers[typeof name], - ) { + on(name: TOpName, handler: ClientEventHandlers[typeof name]) { this.#emitter.on(name, handler) } - once( - name: TOpName, - handler: ClientEventHandlers[typeof name], - ) { + once(name: TOpName, handler: ClientEventHandlers[typeof name]) { this.#emitter.once(name, handler) } - off( - name: TOpName, - handler: ClientEventHandlers[typeof name], - ) { + off(name: TOpName, handler: ClientEventHandlers[typeof name]) { this.#emitter.off(name, handler) } send(packet: Packet) { return new Promise((resolve, reject) => { try { - this.#throwIfDisconnected( - 'Cannot send packet to client that has already disconnected', - ) + this.#throwIfDisconnected('Cannot send packet to client that has already disconnected') - this.#socket.send(serializePacket(packet), err => - err ? reject(err) : resolve(), - ) + this.#socket.send(serializePacket(packet), err => (err ? reject(err) : resolve())) } catch (e) { reject(e) } @@ -91,16 +77,12 @@ export default class Client { } async disconnect(reason: DisconnectReason = DisconnectReason.Generic) { - this.#throwIfDisconnected( - 'Cannot disconnect client that has already disconnected', - ) + this.#throwIfDisconnected('Cannot disconnect client that has already disconnected') try { await this.send({ op: ServerOperation.Disconnect, d: { reason } }) } catch (err) { - throw new Error( - `Cannot send disconnect reason to client ${this.id}: ${err}`, - ) + throw new Error(`Cannot send disconnect reason to client ${this.id}: ${err}`) } finally { this.forceDisconnect(reason) } @@ -173,10 +155,7 @@ export default class Client { if (Date.now() - this.lastHeartbeat > 0) { // TODO: put into config // 5000 is extra time to account for latency - const interval = setTimeout( - () => this.disconnect(DisconnectReason.TimedOut), - 5000, - ) + const interval = setTimeout(() => this.disconnect(DisconnectReason.TimedOut), 5000) this.once('heartbeat', () => clearTimeout(interval)) // This should never happen but it did in my testing so I'm adding this just in case @@ -208,11 +187,9 @@ export type ClientEventName = keyof typeof ClientOperation export type ClientEventHandlers = { [K in Uncapitalize]: ( packet: ClientPacketObject]>, - ) => Promise | void + ) => Promise | unknown } & { - ready: () => Promise | void - packet: ( - packet: ClientPacketObject, - ) => Promise | void - disconnect: (reason: DisconnectReason) => Promise | void + ready: () => Promise | unknown + packet: (packet: ClientPacketObject) => Promise | unknown + disconnect: (reason: DisconnectReason) => Promise | unknown } diff --git a/apis/websocket/src/events/index.ts b/apis/websocket/src/events/index.ts index eb97665..9f1ed4f 100755 --- a/apis/websocket/src/events/index.ts +++ b/apis/websocket/src/events/index.ts @@ -3,7 +3,7 @@ import type { Wit } from 'node-wit' import type { Worker as TesseractWorker } from 'tesseract.js' import { ClientPacketObject } from '../classes/Client.js' import type { Config } from '../utils/getConfig.js' -import type { Logger } from '../utils/logger.js' +import type { Logger } from '@revanced/bot-shared' export { default as parseTextEventHandler } from './parseText.js' export { default as parseImageEventHandler } from './parseImage.js' diff --git a/apis/websocket/src/events/parseImage.ts b/apis/websocket/src/events/parseImage.ts index 742f097..61fa14a 100755 --- a/apis/websocket/src/events/parseImage.ts +++ b/apis/websocket/src/events/parseImage.ts @@ -14,13 +14,8 @@ const parseImageEventHandler: EventHandler = async ( d: { image_url: imageUrl, id }, } = packet - logger.debug( - `Client ${client.id} requested to parse image from URL:`, - imageUrl, - ) - logger.debug( - `Queue currently has ${queue.remaining}/${config.ocrConcurrentQueues} items in it`, - ) + logger.debug(`Client ${client.id} requested to parse image from URL:`, imageUrl) + logger.debug(`Queue currently has ${queue.remaining}/${config.ocrConcurrentQueues} items in it`) if (queue.remaining < config.ocrConcurrentQueues) queue.shift() await queue.wait() @@ -30,10 +25,7 @@ const parseImageEventHandler: EventHandler = async ( const { data, jobId } = await tesseractWorker.recognize(imageUrl) - logger.debug( - `Recognized image from URL for client ${client.id} (job ${jobId}):`, - data.text, - ) + logger.debug(`Recognized image from URL for client ${client.id} (job ${jobId}):`, data.text) await client.send({ op: ServerOperation.ParsedImage, d: { @@ -42,10 +34,7 @@ const parseImageEventHandler: EventHandler = async ( }, }) } catch { - logger.error( - `Failed to parse image from URL for client ${client.id}:`, - imageUrl, - ) + logger.error(`Failed to parse image from URL for client ${client.id}:`, imageUrl) await client.send({ op: ServerOperation.ParseImageFailed, d: { diff --git a/apis/websocket/src/events/parseText.ts b/apis/websocket/src/events/parseText.ts index 7818036..0d9246f 100755 --- a/apis/websocket/src/events/parseText.ts +++ b/apis/websocket/src/events/parseText.ts @@ -4,10 +4,7 @@ import { inspect as inspectObject } from 'node:util' import type { EventHandler } from './index.js' -const parseTextEventHandler: EventHandler = async ( - packet, - { witClient, logger }, -) => { +const parseTextEventHandler: EventHandler = async (packet, { witClient, logger }) => { const { client, d: { text, id }, diff --git a/apis/websocket/src/index.ts b/apis/websocket/src/index.ts index 108341a..d04bdfc 100755 --- a/apis/websocket/src/index.ts +++ b/apis/websocket/src/index.ts @@ -9,25 +9,21 @@ import { inspect as inspectObject } from 'node:util' import Client from './classes/Client.js' -import { - EventContext, - parseImageEventHandler, - parseTextEventHandler, -} from './events/index.js' +import { EventContext, parseImageEventHandler, parseTextEventHandler } from './events/index.js' -import { - DisconnectReason, - HumanizedDisconnectReason, -} from '@revanced/bot-shared' +import { DisconnectReason, HumanizedDisconnectReason, createLogger } from '@revanced/bot-shared' import { WebSocket } from 'ws' -import { checkEnv, getConfig, logger } from './utils/index.js' +import { checkEnvironment, getConfig } from './utils/index.js' + +// Load config, init logger, check environment -// Check environment variables and load config -const environment = checkEnv(logger) const config = getConfig() +const logger = createLogger('websocket-api', { + level: config.consoleLogLevel === 'none' ? 'error' : config.consoleLogLevel, + silent: config.consoleLogLevel === 'none', +}) -if (!config.debugLogsInProduction && environment === 'production') - logger.debug = () => {} +checkEnvironment(logger) // Workers and API clients @@ -71,46 +67,25 @@ const server = fastify() clients.add(client) logger.debug(`Client ${client.id}'s instance has been added`) - logger.info( - `New client connected (now ${clients.size} clients) with ID:`, - client.id, - ) + logger.info(`New client connected (now ${clients.size} clients) with ID:`, client.id) client.on('disconnect', reason => { clients.delete(client) - logger.info( - `Client ${client.id} disconnected because client ${HumanizedDisconnectReason[reason]}`, - ) + logger.info(`Client ${client.id} disconnected because client ${HumanizedDisconnectReason[reason]}`) }) - client.on('parseText', async packet => - parseTextEventHandler(packet, eventContext), - ) + client.on('parseText', async packet => parseTextEventHandler(packet, eventContext)) - client.on('parseImage', async packet => - parseImageEventHandler(packet, eventContext), - ) + client.on('parseImage', async packet => parseImageEventHandler(packet, eventContext)) + + if (['debug', 'silly'].includes(config.consoleLogLevel)) { + logger.debug('Debug logs enabled, attaching debug events...') - if ( - environment === 'development' && - !config.debugLogsInProduction - ) { - logger.debug( - 'Running development mode or debug logs in production is enabled, attaching debug events...', - ) client.on('packet', ({ client, ...rawPacket }) => - logger.debug( - `Packet received from client ${client.id}:`, - inspectObject(rawPacket), - ), + logger.debug(`Packet received from client ${client.id}: ${inspectObject(rawPacket)}`), ) - client.on('heartbeat', () => - logger.debug( - 'Heartbeat received from client', - client.id, - ), - ) + client.on('heartbeat', () => logger.debug('Heartbeat received from client', client.id)) } } catch (e) { if (e instanceof Error) logger.error(e.stack ?? e.message) @@ -125,22 +100,19 @@ const server = fastify() return connection.socket.terminate() } - if (client.disconnected === false) - client.disconnect(DisconnectReason.ServerError) + if (client.disconnected === false) client.disconnect(DisconnectReason.ServerError) else client.forceDisconnect() clients.delete(client) - logger.debug( - `Client ${client.id} disconnected because of an internal error`, - ) + logger.debug(`Client ${client.id} disconnected because of an internal error`) } }) }) // Start the server -logger.debug('Starting with these configurations:', inspectObject(config)) +logger.debug(`Starting with these configurations: ${inspectObject(config)}`, ) await server.listen({ host: config.address ?? '0.0.0.0', @@ -150,8 +122,4 @@ await server.listen({ const addressInfo = server.server.address() if (!addressInfo || typeof addressInfo !== 'object') logger.debug('Server started, but cannot determine address information') -else - logger.info( - 'Server started at:', - `${addressInfo.address}:${addressInfo.port}`, - ) +else logger.info(`Server started at: ${addressInfo.address}:${addressInfo.port}`) diff --git a/apis/websocket/src/utils/checkEnv.ts b/apis/websocket/src/utils/checkEnv.ts deleted file mode 100755 index 3c503e2..0000000 --- a/apis/websocket/src/utils/checkEnv.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Logger } from './logger.js' - -export default function checkEnv(logger: Logger) { - if (!process.env['NODE_ENV']) - logger.warn('NODE_ENV not set, defaulting to `development`') - const environment = (process.env['NODE_ENV'] ?? - 'development') as NodeEnvironment - - if (!['development', 'production'].includes(environment)) { - logger.error( - 'NODE_ENV is neither `development` nor `production`, unable to determine environment', - ) - logger.info('Set NODE_ENV to blank to use `development` mode') - process.exit(1) - } - - logger.info(`Running in ${environment} mode...`) - - if (environment === 'production' && process.env['IS_USING_DOT_ENV']) { - logger.warn( - 'You seem to be using .env files, this is generally not a good idea in production...', - ) - } - - if (!process.env['WIT_AI_TOKEN']) { - logger.error('WIT_AI_TOKEN is not defined in the environment variables') - process.exit(1) - } - - return environment -} diff --git a/apis/websocket/src/utils/checkEnvironment.ts b/apis/websocket/src/utils/checkEnvironment.ts new file mode 100755 index 0000000..803d94e --- /dev/null +++ b/apis/websocket/src/utils/checkEnvironment.ts @@ -0,0 +1,23 @@ +import type { Logger } from '@revanced/bot-shared' + +export default function checkEnvironment(logger: Logger) { + if (!process.env['NODE_ENV']) logger.warn('NODE_ENV not set, defaulting to `development`') + const environment = (process.env['NODE_ENV'] ?? 'development') as NodeEnvironment + + if (!['development', 'production'].includes(environment)) { + logger.error('NODE_ENV is neither `development` nor `production`, unable to determine environment') + logger.info('Set NODE_ENV to blank to use `development` mode') + process.exit(1) + } + + logger.info(`Running in ${environment} mode...`) + + if (environment === 'production' && process.env['IS_USING_DOT_ENV']) { + logger.warn('You seem to be using .env files, this is generally not a good idea in production...') + } + + if (!process.env['WIT_AI_TOKEN']) { + logger.error('WIT_AI_TOKEN is not defined in the environment variables') + process.exit(1) + } +} diff --git a/apis/websocket/src/utils/getConfig.ts b/apis/websocket/src/utils/getConfig.ts index f3168cb..841c219 100755 --- a/apis/websocket/src/utils/getConfig.ts +++ b/apis/websocket/src/utils/getConfig.ts @@ -22,17 +22,14 @@ type BaseTypeOf = T extends (infer U)[] ? { [K in keyof T]: T[K] } : T -export type Config = Omit< - BaseTypeOf, - '$schema' -> +export type Config = Omit, '$schema'> export const defaultConfig: Config = { address: '127.0.0.1', port: 80, ocrConcurrentQueues: 1, clientHeartbeatInterval: 60000, - debugLogsInProduction: false, + consoleLogLevel: 'info', } export default function getConfig() { diff --git a/apis/websocket/src/utils/index.ts b/apis/websocket/src/utils/index.ts index 2dcb7a6..4aab21d 100755 --- a/apis/websocket/src/utils/index.ts +++ b/apis/websocket/src/utils/index.ts @@ -1,3 +1,2 @@ export { default as getConfig } from './getConfig.js' -export { default as checkEnv } from './checkEnv.js' -export { default as logger } from './logger.js' +export { default as checkEnvironment } from './checkEnvironment.js' diff --git a/apis/websocket/src/utils/logger.ts b/apis/websocket/src/utils/logger.ts deleted file mode 100755 index 88c870d..0000000 --- a/apis/websocket/src/utils/logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Chalk } from 'chalk' - -const chalk = new Chalk() -const logger = { - debug: (...args) => console.debug(chalk.gray('DEBUG:', ...args)), - info: (...args) => - console.info(chalk.bgBlue.whiteBright(' INFO '), ...args), - warn: (...args) => - console.warn( - chalk.bgYellow.blackBright.bold(' WARN '), - chalk.yellowBright(...args), - ), - error: (...args) => - console.error( - chalk.bgRed.whiteBright.bold(' ERROR '), - chalk.redBright(...args), - ), - log: console.log, -} satisfies Logger - -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'log' -export type LogFunction = (...x: unknown[]) => void -export type Logger = Record - -export default logger diff --git a/bun.lockb b/bun.lockb index ecb2936..d3eac3e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 210afe1..718ff0c 100755 --- a/package.json +++ b/package.json @@ -1,38 +1,11 @@ { "name": "revanced-helper", "version": "0.0.0", - "description": "🤖 Bots assisting ReVanced on multiple platforms", - "private": true, - "workspaces": [ - "apis/*", - "bots/*", - "packages/*" - ], - "scripts": { - "build": "turbo run build", - "watch": "turbo run watch", - "format": "prettier --ignore-path .gitignore --write .", - "format:check": "prettier --ignore-path .gitignore --cache --check .", - "lint": "eslint --ignore-path .gitignore --cache .", - "lint:apply": "eslint --ignore-path .gitignore --fix .", - "commitlint": "commitlint --edit", - "t": "turbo run", - "prepare": "lefthook install" - }, + "author": "Palm (https://github.com/PalmDevs)", "repository": { "type": "git", "url": "git+https://github.com/revanced/revanced-helper.git" }, - "author": "Palm (https://github.com/PalmDevs)", - "contributors": [ - "Palm (https://github.com/PalmDevs)", - "ReVanced (https://github.com/revanced)" - ], - "license": "GPL-3.0-or-later", - "bugs": { - "url": "https://github.com/revanced/revanced-helper/issues" - }, - "homepage": "https://github.com/revanced/revanced-helper#readme", "devDependencies": { "@biomejs/biome": "1.3.3", "@commitlint/cli": "^18.4.3", @@ -48,9 +21,36 @@ "turbo": "^1.10.16", "typescript": "^5.3.2" }, + "bugs": { + "url": "https://github.com/revanced/revanced-helper/issues" + }, + "contributors": [ + "Palm (https://github.com/PalmDevs)", + "ReVanced (https://github.com/revanced)" + ], + "description": "🤖 Bots assisting ReVanced on multiple platforms", + "homepage": "https://github.com/revanced/revanced-helper#readme", + "license": "GPL-3.0-or-later", "overrides": { "uuid": ">=9.0.0", "isomorphic-fetch": ">=3.0.0" }, - "trustedDependencies": ["lefthook", "biome", "turbo"] + "private": true, + "scripts": { + "build": "turbo run build", + "watch": "turbo run watch", + "format": "prettier --ignore-path .gitignore --write .", + "format:check": "prettier --ignore-path .gitignore --cache --check .", + "lint": "eslint --ignore-path .gitignore --cache .", + "lint:apply": "eslint --ignore-path .gitignore --fix .", + "commitlint": "commitlint --edit", + "t": "turbo run", + "prepare": "lefthook install" + }, + "trustedDependencies": ["lefthook", "biome", "turbo"], + "workspaces": [ + "apis/*", + "bots/*", + "packages/*" + ] } diff --git a/packages/shared/package.json b/packages/shared/package.json index f877e75..19746ba 100755 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -31,7 +31,9 @@ "homepage": "https://github.com/revanced/revanced-helper#readme", "dependencies": { "bson": "^6.2.0", + "chalk": "^5.3.0", "valibot": "^0.21.0", + "winston": "^3.11.0", "zod": "^3.22.4" } } diff --git a/packages/shared/src/constants/HumanizedDisconnectReason.ts b/packages/shared/src/constants/HumanizedDisconnectReason.ts index 0687d98..fb0c070 100755 --- a/packages/shared/src/constants/HumanizedDisconnectReason.ts +++ b/packages/shared/src/constants/HumanizedDisconnectReason.ts @@ -7,8 +7,7 @@ const HumanizedDisconnectReason = { [DisconnectReason.InvalidPacket]: 'has sent invalid packet', [DisconnectReason.Generic]: 'has been disconnected for unknown reasons', [DisconnectReason.TimedOut]: 'has timed out', - [DisconnectReason.ServerError]: - 'has been disconnected due to an internal server error', + [DisconnectReason.ServerError]: 'has been disconnected due to an internal server error', [DisconnectReason.NeverConnected]: 'had never connected to the server', } as const satisfies Record diff --git a/packages/shared/src/schemas/Packet.ts b/packages/shared/src/schemas/Packet.ts index 6c133b0..9e41554 100755 --- a/packages/shared/src/schemas/Packet.ts +++ b/packages/shared/src/schemas/Packet.ts @@ -15,11 +15,7 @@ import { // merge } from 'valibot' import DisconnectReason from '../constants/DisconnectReason.js' -import { - ClientOperation, - Operation, - ServerOperation, -} from '../constants/Operation.js' +import { ClientOperation, Operation, ServerOperation } from '../constants/Operation.js' /** * Schema to validate packets @@ -59,10 +55,7 @@ export const PacketDataSchemas = { labels: array( object({ name: string(), - confidence: special( - input => - typeof input === 'number' && input >= 0 && input <= 1, - ), + confidence: special(input => typeof input === 'number' && input >= 0 && input <= 1), }), ), }), diff --git a/packages/shared/src/utils/guard.ts b/packages/shared/src/utils/guard.ts index 86ec59f..b58983b 100755 --- a/packages/shared/src/utils/guard.ts +++ b/packages/shared/src/utils/guard.ts @@ -1,8 +1,4 @@ -import { - ClientOperation, - Operation, - ServerOperation, -} from '../constants/Operation.js' +import { ClientOperation, Operation, ServerOperation } from '../constants/Operation.js' import { Packet } from '../schemas/Packet.js' /** @@ -11,10 +7,7 @@ import { Packet } from '../schemas/Packet.js' * @param packet A packet * @returns Whether this packet is trying to do the operation given */ -export function packetMatchesOperation( - op: TOp, - packet: Packet, -): packet is Packet { +export function packetMatchesOperation(op: TOp, packet: Packet): packet is Packet { return packet.op === op } @@ -23,9 +16,7 @@ export function packetMatchesOperation( * @param packet A packet * @returns Whether this packet is a client packet */ -export function isClientPacket( - packet: Packet, -): packet is Packet { +export function isClientPacket(packet: Packet): packet is Packet { return packet.op in ClientOperation } @@ -34,8 +25,6 @@ export function isClientPacket( * @param packet A packet * @returns Whether this packet is a server packet */ -export function isServerPacket( - packet: Packet, -): packet is Packet { +export function isServerPacket(packet: Packet): packet is Packet { return packet.op in ServerOperation } diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 47ea1c6..9d276ca 100755 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './guard.js' +export * from './logger.js' export * from './serialization.js' export * from './string.js' diff --git a/packages/shared/src/utils/logger.ts b/packages/shared/src/utils/logger.ts new file mode 100644 index 0000000..085de1c --- /dev/null +++ b/packages/shared/src/utils/logger.ts @@ -0,0 +1,66 @@ +import { createLogger as createWinstonLogger, LoggerOptions, transports, format } from 'winston' +import { Chalk, ChalkInstance } from 'chalk' + +const chalk = new Chalk() + +const LevelPrefixes = { + error: `${chalk.bgRed.whiteBright(' ERR! ')} `, + warn: `${chalk.bgYellow.black(' WARN ')} `, + info: `${chalk.bgBlue.whiteBright(' INFO ')} `, + log: chalk.reset(''), + debug: chalk.gray('DEBUG: '), + silly: chalk.gray('SILLY: '), +} as Record + +const LevelColorFunctions = { + error: chalk.redBright, + warn: chalk.yellowBright, + info: chalk.cyanBright, + log: chalk.reset, + debug: chalk.gray, + silly: chalk.gray, +} as Record + +export function createLogger( + serviceName: string, + config: SafeOmit< + LoggerOptions, + | 'defaultMeta' + | 'exceptionHandlers' + | 'exitOnError' + | 'handleExceptions' + | 'handleRejections' + | 'levels' + | 'rejectionHandlers' + >, +) { + const logger = createWinstonLogger({ + exitOnError: false, + defaultMeta: { serviceName }, + handleExceptions: true, + handleRejections: true, + transports: config.transports ?? [ + new transports.Console(), + new transports.File({ + dirname: 'logs', + filename: `${serviceName}-${Date.now()}.log`, + format: format.combine( + format.uncolorize(), + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.printf( + ({ level, message, timestamp }) => `[${timestamp}] ${level.toUpperCase()}: ${message}`, + ), + ), + }), + ], + format: format.printf(({ level, message }) => LevelPrefixes[level] + LevelColorFunctions[level]!(message)), + ...config, + }) + + logger.silly(`Logger for ${serviceName} created at ${Date.now()}`) + + return logger +} + +type SafeOmit = Omit +export type Logger = ReturnType