From 17c6be7bee5b5c24fd4a5279e73374b0bb7a6229 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 29 Nov 2023 00:52:17 +0700 Subject: [PATCH] feat(packages/shared): add logger factory - @revanced/websocket-api now also utilizes the new logger from the shared package - @revanced/websocket-api/utils/checkEnv has been renamed to its full form - It also no longer returns anything as it's no longer needed --- apis/websocket/config.json | 2 +- apis/websocket/config.schema.json | 9 +- apis/websocket/src/classes/Client.ts | 49 +++-------- apis/websocket/src/events/index.ts | 2 +- apis/websocket/src/events/parseImage.ts | 19 +---- apis/websocket/src/events/parseText.ts | 5 +- apis/websocket/src/index.ts | 78 ++++++------------ apis/websocket/src/utils/checkEnv.ts | 31 ------- apis/websocket/src/utils/checkEnvironment.ts | 23 ++++++ apis/websocket/src/utils/getConfig.ts | 7 +- apis/websocket/src/utils/index.ts | 3 +- apis/websocket/src/utils/logger.ts | 25 ------ bun.lockb | Bin 332868 -> 340420 bytes package.json | 58 ++++++------- packages/shared/package.json | 2 + .../constants/HumanizedDisconnectReason.ts | 3 +- packages/shared/src/schemas/Packet.ts | 11 +-- packages/shared/src/utils/guard.ts | 19 +---- packages/shared/src/utils/index.ts | 1 + packages/shared/src/utils/logger.ts | 66 +++++++++++++++ 20 files changed, 179 insertions(+), 234 deletions(-) delete mode 100755 apis/websocket/src/utils/checkEnv.ts create mode 100755 apis/websocket/src/utils/checkEnvironment.ts delete mode 100755 apis/websocket/src/utils/logger.ts create mode 100644 packages/shared/src/utils/logger.ts 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 ecb2936d05f5cf49a1cbbd14d910bf728e9e3e3d..d3eac3eb57244a5e7255a9e836a936c8c1c4a1bd 100755 GIT binary patch delta 17154 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 Bbq@dl delta 13024 zcmX@|SmekGkqLU5wNr#l8w)SVpW3#yW{=gYR9lq^JEKnQ6tCZUOa8+>hPhjWSs6gU zYht)O(;|nB6%AsO7l<3wU%tV>z|X+Y(0>g=@4LakAj-hdP@0#LnViYM@bxA{Jo_dC z10MrJ!`d4R41x>{4ShEl7~~lk8X|5oFbFd+G*m+MH9*a&Wnf^CW?*O#zQe%4!@$r` zm6Ms2Rm{M!=MF^u>N{X_oEv691?uiFFi0^lG?d(7VBlh4XlT9%p-s-|fq|QWp&|axZWBD6_7Z# z7CZunNJA&o>e9S|%z{(~hSWz43<3-c4axcW*_o*f43SWIw?~kWEPMd*Qc+^LZc=_q zp5WnmL#60g05WnYqfarVl9>Nd*07*io9~caC--_II*lO6XXRmsD*;>7#LI; z7#f)0K~llHw-A>z-hre!-k%WhPq#N`N!N)jo?yhx@$1dMyl<-yp4>dabio!zhRGWr znseq{XJ9a3U}#{QZ0KRmI_EkAgVSVPTWijHP;vIjhBwWb_-;(j`e4WEbc2Dxd~&Xx zHS41r3=GzjbNg&KZZa@fGcYtTf%J1!-eh2KXJBZUKGBlNVDbVp4yFS)C$IWw$I5bx zfx#ARu;VQT1{Vf~2FA%79nG0qZ%xknWXF2(76U`n>bX#5mc|(wtND8O%K!{meOBpFvc@(*$S#Gq_2X z<{amts^RiX|DR1>_1BKm@HxyF!<*)uiO(U%FoJ`B{&NNfD^N=0y!o7g!3-SZh92fj z{4XZI`e(=C_=15!2aygham!<=)+Hp32fY=R55=TEke8d1sO`NPBVdjENa{LI9gM>@tM+Sx{# z%D@l>aX^X%$5%)!u}8kMn;hUwHuXEia&Z1+z5AVkA#<{>jWws+4@jZQ49e(0=7~ELF^(#XFl#YU`C5C2VU|@)0Wnci&aZr6E z(De-L3=9mZP>skm$iQ@{IEV)EGohZ$VgR}A1i!9lK z(IAW0vx1vY47*t&9&Ge8+wX^;axLKHF})1Zv;1u71rLHzGfi+(`GK{UvspHM!C289#@8zjRp zf>J)H3VaLQf-Pe`LoAKuKnXXLX(|3Rb{(=N`vEOuW-$-I=@@?@wRp&J;fVgaZ@L^f{JHK8z2iM|v=YPp@!f;+?+NgUOrm@pMj4 zrts++PE5Shb3K{78J|wy2@<#g5|H&`@@9NKJ=2RRe0qm76Yuo1Ac2?DCB2!#r(3u% z@lLPxX7Xlw%{2X#H&fX37a$Q`A0}_6w@lNke3-(f&v0epoqiW2@}6nBmM>G-bPqRB zkoYorGks*5ehVb>10-VW$K=iQnQ3~LA5+-$74D!w0f~HNnr`LK6gEA=0~9L$Ox{f2 znWn!2iEwy=f+c{-o9QRh^jQH+VbgbjME-(Aeltz?3S+!A#zajMF27K|$jK3L20A^K{M-P|)~-f+mE?n~`<; zPLRM2kbrC`lQ$##^vqCD(D;FZ1|+~aT`~+5H2$EV31jkR3FUmmmR-5Kx#zGkG&gPM;YKN-7|MzaRnW>7Fs5q!J1W zkr*a#M%n2`wCpa6+w@@ABuzA_e+R6qi}aZKKfiqj+GK;aP%3J;Kg@^sF4P;5ce-mTlQ*OJ^oglV;nO+dnRusv1qoPAcT58X2uNUQ z8k0Ap_4JP*fs6zu-sz#~Ox}#P(-)>Qg-@4AWa6F9n!)7FXg@tL0~8=2fvq3`$LWlj zpa4l?;+>wF$>h!GJbfcbKqHxnce-d6lQ*O5^u#Qt@aZ=|0!Kjt?$ZUcK>?D=t- zo5`EebNj(;CU>jJ3(Qz}VIzu}c`2zC)89{I+71%tDlN?f4b?H!JedBki0RvOfqteQ zki_I#r2q`@K#3?HHD_!%Jm5RmXE zsF(l)1A_ts#G{{?7{L7-K?ZQ29NbCz1yv`+z`$S#(#XKT@Eaml4;ttO4HJNc|3EDk zVPIeYwYouq{~!uM1LzDlKw-te!0;a`CdRX@+!eL-w;DstwWME)8 z3KipniYYNLFdSoobZGdYV#*8*41YlS7#J7?m?6Qh!oa}52x>ktFffQSLmZ_FiXy0( zJXCMJ8Uq6ZGgMFkDyYuDz`)7~=^iRV#WWZg7}%idl%S5*WME*p1_?k06=p~@Xn~>* zDy9zAs}1r9R7?XZro+I%a2zy}4ieXd3hFX2FdSw8kBl>DL4Bgfz`(ErfvVGI zU|`q@6*Gd0889#~w1Ny{U|=wYih&YaJOg;#nZX1qX2bv<2?nP-Q|5YztBo1J;~8K< z3#dX92Jq+w*hovLm?fBg}OB;Awk6)q3UcH7#PZ+20B5- zY#A6BnxJCN&?vEEU|;~vRDn{w8&sV=C=0SMFw}!mygO9Tfq{Wx9Wsi3_P7FR!bxsTn3>%p+Vygn(Ts#1w+L=7#J92pkkp=13|^IEL1EED(1z&z#s<|3x|q%g9;cV8hZjUeHj=SR6q;{28Jl8LO%uu1|6taG*rwVRKh{UVxVFH3=9k= zP_bC3SRexfgDF%j4k{MJz`$Sy5(CA5JXA24fq}smB*?(PkN_160gYoZGBAMhb0SnM zl!1Z45vnc;Di+4Tz~BrOONNG4I0FNND^x5MsxAVQ`k`WJ%nS_mAa*1u^@A7;3=HW| zg;Ai?4;9ORibXRpF!(_olL-}zVPIegfr@28#bOy47{Z}q*-)`K1_p*msChY12gZZ) ze;ia{9#mlhsH}!6%!i64GB7Z_V_;wa6*vV@u_OithEw3o$-qzu6$4GJfyxR{IZ^}_ z1GP&)Wd%sA7%G+uO6Xv{^$ZLpP}ih^T*v?(Dokf!U;z0VRA`h##WEPcW455V z9fk_1SSAAl11N!k%&dfpWr2!dHt;w%Llsmkn}Gp5>H{hZszG9)_|IWrU;y=!LHW4` zB*?(PkP9851Lfyhs8}8-J3+H&9aIc7^R}A>JZQ}Tt4j(P7#Q|I#TubyKp_JI!(OP~ z7O37L1_p-utt<=-pgi6R6)a|8VA#k49xrETgNlJ>3PGtJwt=t zF)%RnL&Z9wV&x1B4Ck1@Bfkt?P_YUI28IAgSy9i>4Hc{ejjA#+Fo4PekinoNP{qK& zkO>v*g{rG&U|`tF1S!M&pkg(kN{10rgo7La(hHg_Z)AcL;S-?h>KGUpN+1?9Oaw=J zJtzq>FjO%?;&d`pVFRerV1mTy6sTAusO)C~PrNZqg^D#XFfgnJ*~Y-YFbyi!%)r2~ z1}ZikD%Ju@aEuHLpt5BKR18#E?EvRL1_p+iP{B3^28LaX;Ng3QSx~Wd1_p+uObiU5 z0%tZ7pVFnX;3W8w{RIHPMfng#O0|ThQnF|%`0+j(w;Asqoc~G%#1_lNjCI$vj zc~d_hD%b-`a8Oq-00#{NLoWjZ10NH37Jy+9R9zpa+J%ZOhKltwFff491E|1R0u`G8 zYNj!QXC4@qLd7OBFfe>)WMBZ5H_ISm^$e337#RLBf@d!nmO}&?CNnTFe1tk?1ypPb z0|Ub+sMt!V*i=wC4~@@NP_b#C#wb*5HB@Xms4dJ0$;xY>o}0nIz>vmR&%gjGnbtuS z&SYR^wh<~ehk=0sltn?gWD`_uE~rh&z`y`1tTscl z;yeZhhE*U%3=9n0KzSBq&U^+222imHDulK}H7{U*#1W{#*#UL+LIwtgD=Z8QpnSgz zs%{ae83q;G4Ha9=z`$^Xg@FNF7C^<8fSO@YvAt09>X$MwFo04xDE{^{GcbVK4$BxA z7|t^>Fo5#z5vbDh|sj;q8ZK&8bP^}1ZBm)D(9jMrLP^hvnFo2@wE>vs> zsQf>{!oUEEn|shu0M%BYd=8452T+B(Kn`JMU;v3dgo^EEU|@L62uVzjpkjMKEoept z22e;ohKlV4r3I+i6R6leP+DMQV5kQvdM5_=65I|5DUppbmS%-{`f{~cvuV7SP_zyJ!Ow@`)0 zpb9|-zk`Y$2bJ?I3=AOk@1bHl0w6U`MZ$EcQf9U3H&U2mBtg@Ao~#TE-mDA^KCBE3 zpsu$cD+7c7_P|tTZ^nAi)FEh^5j2qj>S$hOWnj3%%D`}ym4V?JD+9xIRt9iq^Cl|; z!!1?@hTE(R40l)=81Aw%Fx+EhV7Sl9!0?b2($NHUEE8B67!p|-7?M~S7?N2T7*bdn zz*CEXtPBi6tPBjntPBhxtPBjHtPBictPENV;j9b{5v&Xhk*o|14y+6ej;ss}POJ&psBWHtPBjG8Lq>u3=BtE z85oYSGB6xxWnehL%D`}vm4V?DD+9x6RtAPMtPBiiSs55W6OY-f3=Fxf3=9R+4bz#$ zc`I2N7^+wq7;0D<7;2{rrZY>6wzD!Ybg(jj=ReX|85ltGsP@}?vzWIt34(GfD1C!U zW>85yb-HgZv$E+w76yiAObiU5Su4;?)_hh5hDEFl44~;b&}1ZN!Vome*~QAhaF7); z+j+ z4k!(3sDY+VXRtCb%w%O?0GR?(3u=Uc8eRKY7#I$)Fff4HRU4f5aD|D1;WQHi1E_6$nTdhnA`_(Zc7}<80W!Yz|NjLhNF@Mjyj@~qVBlb4U|7w> zz;KU=fdQoF4if{zZKya%-*u?jSD|X|GBGf~)ZBuot7o{u#J~Vj08)IDiGcxG5y)X6 zIgsN#nIV>d90uy0f_j!9i$M-nWoBRijZi&gVqgG`UcF^vU;qtpfd;-nV`ZQ;1sX>K zjjugsVqkd0#K7WFPRt^l$jYAzA-T{d|_f>01ab-+E<_C$PnHdC!(nIYpf?92=d+{_H%K_1ZH4|vQ4 zRGl(2Fo-cTFo=SRPEchJ8nh8%W?&Fxh78t#hN!$jRS+{|SPL}V^_Pi(;U_3(FflNE zXJTLg4U2)s&_H8mFiSw@fX2N*gGC_AK_f~qIgkTDqf(#|EYQdns4oT@_NoV21~Qx( zG`7ObzyNAX!z=_1ZGlFaBAFN%gqax_grGiy845B0WGP6$1TzBz$kCtyBTz^nI|5`W zXmkmr9yA&y#|-g|1gNtQ(g0HcG6Xb41@;-J(FP4xe1?HWnLr@}(u-~uXlTQgnSsHD znSlW`^Z^0}=bgS^StPZ%pTtU@>CqyD?oyl0|}P?TzUMQY;eF zOC(rKnD*V6z6vCH`Ns5n5-bu>#S%;rH>dvrDMr)8lznr0f+UL%6I8t!YytCDA(P&N zPmhQ(F~%9`8R!`xmNM_Un^h;brEn@j2D{ltFf*suNU=yUeY!n;h7^kr6XTuf&!kw4 zU=EvZB+cT-Bz$LjlQfHwOgwb;@~vxpRg1!o)i5!}ndupUY|p$i{Q^h@Y<=^DZ%Y^K zl4WiJyTVM*l7XS_&U85$76~)hifEoy7cN+_%*kM4j5E+PG}bd_fG(PDP-C|%a@zh$ ziit7K6ymo%cc$mauoy8l-i(LBduC>zum!X6*%GSCEX60Z7X2(eyq!773=v zN7G|eStO<(kYkZzN_{l_jvR{*Q|Y7WJt`~`Om&Z@pHpO!m>wg~BE{7CXnKV_ixJb* zN7L8Hvq&&4cr^VOh`;gC^aCm^MofDiP1jRkkzhLYXu5|2ixJb6N7HK*SbSt)tFdR- z=&(LL{QV)w4Th$mP<-=f`W|H#qv>LbEK*Fr9!)n=WHFL~E$U8+T;90hS-%Anqk*0= zNHxRO$I}}WS&YzJHw%p~F@25_iwVq?(_eu&8y`(qP-c;UI!R)BkTOdK6V!I5=4aDC zD6{x5@x7dG2lBM|%jppy|DXi|6U3jhpdpq9x8aT|ixfx_WW#g;HI^p0%o;ToH?FQX z;6d{Si0ehC3#zl2gUs+!XTfcT!E`+hmLOzDBM1F94YVTS0#P`jW%pYxpF=X&#&_ zPCc6bPLsumY1X^xN?I&NGO#u1nWq|lvUx}OgIx-a%SZ307ih6an86l}pYLD(<;sP9 zZ@?Z59b8?+?@aKy>(r=>eiF5@xV9?z-XOywOvw_JeH&`>_nV8s1Lpk?@-p zzvRHSg3W}P0d@KG1KKQpOfZY)eV(qR!(zl_{$+X`$YS>|(E($7;zVfK}O6B#95{n zhA<0H*E3*cpT1X*MRocMJ(dGp3E~j>e*)V#>9dqw-#&wfl|xb_F)dZMxFj(t2SgQR z=A~z*Rc@cJ%4)zkeZvD5*6EELtiIdNtFazn=W-AP^|u)qA_S-J(q~nhzE6);ihBW6 z&_IY4$=bhqted$KgdplC2!Ygxao>RQIfM~Pleio}qy3;UHsR^B3|P&$8le0w!qaaV Hu)YESigtif 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