diff --git a/.env.example b/.env.example index ad23248..8268172 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,17 @@ HOST= # development, production NODE_ENV= + +# Logger level, defaults to info. Set this to debug to get more information. +LOG_LEVEL= + +# Command prefix. Default is '@@'. Make sure it's set or there will be errors. +COMMAND_PREFIX=@@ + +# PROGRAM WILL ERROR IF NOT SET. +TWITCH_CLIENT_TOKEN= +TWITCH_CLIENT_SECRET= +TWITCH_BOT_ACCESS_TOKEN= +TWITCH_BOT_REFRESH_TOKEN= +TWITCH_BOT_NAME= +TWITCH_BOT_ID= diff --git a/Index.ts b/Index.ts index 76fe9f2..117c842 100644 --- a/Index.ts +++ b/Index.ts @@ -1,15 +1,58 @@ // Load env variables import { env } from '@adonisjs/env/build/standalone' -import { readFileSync } from 'fs' +import { Logger } from '@adonisjs/logger/build/standalone' +import { TwitchAuth } from 'befriendlier-shared' +import { readdirSync, readFileSync } from 'fs' import path from 'path' +import TwitchConfig from './config/Twitch' +import WSConfig from './config/Ws' +import packageJSON from './package.json' +import Twitch from './src/Twitch' import Ws from './src/Ws' env.process(readFileSync(path.join(__dirname, '.env'), 'utf-8')) -// Start Ws client. -;(() => new Ws( - `ws://${env.getOrFail('HOST') as string}:${env.getOrFail('PORT') as string}`, - { - 'user-agent': `BEFRIENDLIER-BOT-${Date.now()}`, - }, -))() +const logger = new Logger({ + name: 'befriendlier-bot', + enabled: true, + level: typeof env.get('LOG_LEVEL') === 'string' ? String(env.get('LOG_LEVEL')) : 'info', + prettyPrint: env.get('NODE_ENV') === 'development', +}) + +// Initialize config values for WS. +const wsConfig = new WSConfig(env) +const server = new Ws(wsConfig, logger) + +// Initialize config values for Twitch. +const apiConfig = new TwitchConfig(env) +const api = new TwitchAuth(apiConfig, logger.level) + +// Start Twitch client. +const twitch = new Twitch(apiConfig, server, api, packageJSON, logger) + +// Add command handlers +const commandDirectory = path.join(__dirname, 'src', 'Handlers') +const commandFiles = readdirSync(commandDirectory, 'utf-8') + +// eslint-disable-next-line no-void +void (async function loadHandlers (): Promise { + let currentFileDir: string = '' + + try { + for (let index = 0; index < commandFiles.length; index++) { + currentFileDir = commandFiles[index] + const fullFileName = path.join(commandDirectory.toString(), commandFiles[index]) + + // Import + const Command = (await import(fullFileName)).default + twitch.handlers.push(new Command(twitch, server, logger)) + } + + server.connect() + } catch (error) { + logger.error({ err: error }, `Index.ts: Something went wrong while importing ${String(currentFileDir)}.`) + setTimeout(() => { + process.exit(0) + }, 10000) + } +})() diff --git a/config/Twitch.ts b/config/Twitch.ts new file mode 100644 index 0000000..c63e93f --- /dev/null +++ b/config/Twitch.ts @@ -0,0 +1,55 @@ +import { Env } from '@adonisjs/env/build/src/Env' + +export default class TwitchConfig { + public clientToken: string + public clientSecret: string + public superSecret: string + public refreshToken: string + public redirectURI: string + public user: { name: string, id: string } + public scope: string[] + public headers: { 'user-agent': string } + public commandPrefix: string + + constructor (env: Env) { + /** + * Twitch client ID token. + */ + this.clientToken = env.getOrFail('TWITCH_CLIENT_TOKEN') as string + + /** + * Twitch client secret token. + */ + this.clientSecret = env.getOrFail('TWITCH_CLIENT_SECRET') as string + + /** + * Redirect URI. + */ + this.redirectURI = '' + + /** + * Twitch username. + */ + this.user = { + name: env.getOrFail('TWITCH_BOT_NAME') as string, + id: env.getOrFail('TWITCH_BOT_ID') as string, + } + + /** + * Scopes to ask for. + */ + this.scope = ['chat:read', 'chat:edit', 'whispers:read', 'whispers:edit'] + + /** + * HTTP request headers. + */ + this.headers = { + 'user-agent': 'befriendlierapp (https://github.com/kararty/befriendlier-web)', + } + + /** + * Command prefix. + */ + this.commandPrefix = env.getOrFail('COMMAND_PREFIX') as string + } +} diff --git a/config/Ws.ts b/config/Ws.ts new file mode 100644 index 0000000..9f68ad4 --- /dev/null +++ b/config/Ws.ts @@ -0,0 +1,20 @@ +import { Env } from '@adonisjs/env/build/src/Env' + +export default class WSConfig { + public url: string + public headers: { 'user-agent': string } + + constructor (env: Env) { + /** + * Server url to connect to. + */ + this.url = `ws://${env.getOrFail('HOST') as string}:${env.getOrFail('PORT') as string}` + + /** + * HTTP request headers. + */ + this.headers = { + 'user-agent': `BEFRIENDLIER-BOT-${process.platform}-${process.pid}`, + } + } +} diff --git a/package.json b/package.json index 505f4db..87c19b0 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "befriendlier-bot", "version": "0.0.0", - "description": "", + "description": "Twitch bot serving commands to the befriendlier.app service.", "private": true, "scripts": { "build-bot": "tsc && copyfiles .env build/", - "start": "node build/Index.js --inspect", - "start-test": "ts-node-dev --respawn --clear --log-error --inspect=\"9230\" -- Index.ts", + "start": "node build/Index.js", + "start-test": "ts-node-dev --respawn --clear --log-error --inspect=\"0\" -- Index.ts", "release": "standard-version && git push --follow-tags origin master" }, "repository": { @@ -32,13 +32,22 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", + "pino-pretty": "^4.1.0", "standard-version": "^8.0.2", "ts-node-dev": "^1.0.0-pre.56", "typescript": "^3.9.7" }, "dependencies": { "@adonisjs/env": "^1.0.18", - "befriendlier-shared": "github:kararty/BeFriendlier-Shared#semver:^4.1.0", + "@adonisjs/logger": "^2.1.0", + "@adonisjs/validator": "^7.4.0", + "befriendlier-shared": "github:kararty/BeFriendlier-Shared#semver:5.4.1", + "dank-twitch-irc": "github:kararty/dank-twitch-irc", + "p-queue": "^6.6.0", "ws": "^7.3.1" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d56c383..f39a81e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,11 @@ dependencies: '@adonisjs/env': 1.0.18 - befriendlier-shared: github.com/kararty/BeFriendlier-Shared/f0f3c6f06ac55db28453a337908b816cd8fd4362 - ws: 7.3.1 + '@adonisjs/logger': 2.1.0 + '@adonisjs/validator': 7.4.0 + befriendlier-shared: github.com/kararty/BeFriendlier-Shared/d0c50fc044b9d645567d8213258e4f74a48b16b9 + dank-twitch-irc: github.com/kararty/dank-twitch-irc/8dcdac2a6ee0a273a1d0490c59a82bf38e29569c + p-queue: 6.6.0 + ws: 7.3.1_5290a7aab7631971258e1bd11475725e devDependencies: '@types/node': 14.0.27 '@types/ws': 7.2.6 @@ -17,10 +21,14 @@ devDependencies: eslint-plugin-node: 11.1.0_eslint@7.5.0 eslint-plugin-promise: 4.2.1 eslint-plugin-standard: 4.0.1_eslint@7.5.0 + pino-pretty: 4.1.0 standard-version: 8.0.2 ts-node-dev: 1.0.0-pre.56_typescript@3.9.7 typescript: 3.9.7 lockfileVersion: 5.1 +optionalDependencies: + bufferutil: 4.0.1 + utf-8-validate: 5.0.2 packages: /@adonisjs/env/1.0.18: dependencies: @@ -29,6 +37,30 @@ packages: dev: false resolution: integrity: sha512-Zo/dh70DRw8XgYpzL1ygBnzTO0gr7oIyVdcj9QGySE/7+gvtpgENgHZyin5JuefWOCDDERIVkPM86WpXBSPlnA== + /@adonisjs/logger/2.1.0: + dependencies: + '@poppinss/utils': 2.5.2 + '@types/pino': 6.3.0 + abstract-logging: 2.0.0 + pino: 6.4.1 + dev: false + resolution: + integrity: sha512-BLaIdN17RD9wP/X1vl/5P9onnH3gkROCIPQpC8/mtvbMmUbE1NrYaRBCobH2r05AOMu/BfNKgvEDzgfqA8TVcg== + /@adonisjs/validator/7.4.0: + dependencies: + '@poppinss/utils': 2.5.2 + '@types/luxon': 1.24.3 + '@types/validator': 13.1.0 + luxon: 1.24.1 + validator: 13.1.1 + dev: false + peerDependencies: + '@adonisjs/http-server': ^3.0.0 + peerDependenciesMeta: + '@adonisjs/http-server': + optional: true + resolution: + integrity: sha512-DLj6AJze0fUwVNAnzKKgP35zR9G8gxtbddWqbBWjLzabxvKXQAtXHQ79fJ0n9e6JEq1tCRH9Iv/Xf0RdJ4P8Vg== /@babel/code-frame/7.10.4: dependencies: '@babel/highlight': 7.10.4 @@ -47,6 +79,10 @@ packages: dev: true resolution: integrity: sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + /@hapi/bourne/2.0.0: + dev: true + resolution: + integrity: sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== /@poppinss/utils/2.5.2: dependencies: buffer-alloc: 1.2.0 @@ -58,14 +94,51 @@ packages: dev: false resolution: integrity: sha512-NrBtC977jTSyQdkH2YskEsUmJsu7rpZpCf+NZao6wkq/ANaydkVIFdYUmKGd2/K0xzZ/YZmZ+mUtI0m8VlaFFg== + /@sindresorhus/is/3.1.0: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-n4J+zu52VdY43kdi/XdI9DzuMr1Mur8zFL5ZRG2opCans9aiFwkPxHYFEb5Xgy7n1Z4K6WfI4FpqUqsh3E8BPQ== + /@szmarczak/http-timer/4.0.5: + dependencies: + defer-to-connect: 2.0.0 + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + /@types/cacheable-request/6.0.1: + dependencies: + '@types/http-cache-semantics': 4.0.0 + '@types/keyv': 3.1.1 + '@types/node': 14.0.27 + '@types/responselike': 1.0.0 + dev: false + resolution: + integrity: sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== /@types/color-name/1.1.1: dev: true resolution: integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + /@types/debug/4.1.5: + dev: false + resolution: + integrity: sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== + /@types/duplexify/3.6.0: + dependencies: + '@types/node': 14.0.27 + dev: false + resolution: + integrity: sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A== /@types/eslint-visitor-keys/1.0.0: dev: true resolution: integrity: sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + /@types/http-cache-semantics/4.0.0: + dev: false + resolution: + integrity: sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== /@types/json-schema/7.0.5: dev: true resolution: @@ -74,18 +147,53 @@ packages: dev: true resolution: integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + /@types/keyv/3.1.1: + dependencies: + '@types/node': 14.0.27 + dev: false + resolution: + integrity: sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + /@types/luxon/1.24.3: + dev: false + resolution: + integrity: sha512-8lkeHb0Hkpyqmj0lYrZItakKM73jaKTUe4/PMl2e8o96oxTUzagbbz2DUOMn0NpjSovmZQTJvoCheFeI2bwS4g== /@types/minimist/1.2.0: dev: true resolution: integrity: sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= /@types/node/14.0.27: - dev: true resolution: integrity: sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== /@types/normalize-package-data/2.4.0: dev: true resolution: integrity: sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + /@types/pino-std-serializers/2.4.1: + dependencies: + '@types/node': 14.0.27 + dev: false + resolution: + integrity: sha512-17XcksO47M24IVTVKPeAByWUd3Oez7EbIjXpSbzMPhXVzgjGtrOa49gKBwxH9hb8dKv58OelsWQ+A1G1l9S3wQ== + /@types/pino/6.3.0: + dependencies: + '@types/node': 14.0.27 + '@types/pino-std-serializers': 2.4.1 + '@types/sonic-boom': 0.7.0 + dev: false + resolution: + integrity: sha512-AuZ32IbQlzW3XY9jA9X8iXkhMTtloKWHz1Ze23ClPx7L8ES69BNGfH308LsU2tHnDCLLCFoZvMn8+1njBx2EKg== + /@types/responselike/1.0.0: + dependencies: + '@types/node': 14.0.27 + dev: false + resolution: + integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + /@types/sonic-boom/0.7.0: + dependencies: + '@types/node': 14.0.27 + dev: false + resolution: + integrity: sha512-AfqR0fZMoUXUNwusgXKxcE9DPlHNDHQp6nKYUd4PSRpLobF5CCevSpyTEBcVZreqaWKCnGBr9KI1fHMTttoB7A== /@types/strip-bom/3.0.0: dev: true resolution: @@ -94,6 +202,10 @@ packages: dev: true resolution: integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + /@types/validator/13.1.0: + dev: false + resolution: + integrity: sha512-gHUHI6pJaANIO2r6WcbT7+WMgbL9GZooR4tWpuBOETpDIqFNxwaJluE+6rj6VGYe8k6OkfhbHz2Fkm8kl06Igw== /@types/ws/7.2.6: dependencies: '@types/node': 14.0.27 @@ -202,6 +314,10 @@ packages: hasBin: true resolution: integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + /abstract-logging/2.0.0: + dev: false + resolution: + integrity: sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA== /acorn-jsx/5.2.0_acorn@7.3.1: dependencies: acorn: 7.3.1 @@ -288,6 +404,17 @@ packages: dev: true resolution: integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + /args/5.0.1: + dependencies: + camelcase: 5.0.0 + chalk: 2.4.2 + leven: 2.1.0 + mri: 1.1.4 + dev: true + engines: + node: '>= 6.0.0' + resolution: + integrity: sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ== /array-find-index/1.0.2: dev: true engines: @@ -308,6 +435,12 @@ packages: node: '>= 0.4' resolution: integrity: sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + /array-uniq/1.0.2: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0= /array.prototype.flat/1.2.3: dependencies: define-properties: 1.1.3 @@ -335,6 +468,12 @@ packages: node: '>=4' resolution: integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + /atomic-sleep/1.0.0: + dev: false + engines: + node: '>=8.0.0' + resolution: + integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== /balanced-match/1.0.0: dev: true resolution: @@ -379,6 +518,34 @@ packages: dev: true resolution: integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + /bufferutil/4.0.1: + dependencies: + node-gyp-build: 3.7.0 + dev: false + optional: true + requiresBuild: true + resolution: + integrity: sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA== + /cacheable-lookup/5.0.3: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== + /cacheable-request/7.0.1: + dependencies: + clone-response: 1.0.2 + get-stream: 5.1.0 + http-cache-semantics: 4.1.0 + keyv: 4.0.1 + lowercase-keys: 2.0.0 + normalize-url: 4.5.0 + responselike: 2.0.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== /callsites/3.1.0: dev: true engines: @@ -426,6 +593,12 @@ packages: node: '>=4' resolution: integrity: sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + /camelcase/5.0.0: + dev: true + engines: + node: '>=6' + resolution: + integrity: sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== /camelcase/5.3.1: dev: true engines: @@ -481,6 +654,12 @@ packages: dev: true resolution: integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + /clone-response/1.0.2: + dependencies: + mimic-response: 1.0.1 + dev: false + resolution: + integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= /color-convert/1.9.3: dependencies: color-name: 1.1.3 @@ -769,16 +948,20 @@ packages: dev: true resolution: integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + /debug-logger/0.4.1: + dependencies: + debug: 2.6.9 + dev: false + resolution: + integrity: sha1-4zhJw2njzTYbULKZ1xylIkuqGuE= /debug/2.6.9: dependencies: ms: 2.0.0 - dev: true resolution: integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== /debug/4.1.1: dependencies: ms: 2.1.2 - dev: true resolution: integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== /decamelize-keys/1.1.0: @@ -796,10 +979,24 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + /decompress-response/6.0.0: + dependencies: + mimic-response: 3.1.0 + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== /deep-is/0.1.3: dev: true resolution: integrity: sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + /defer-to-connect/2.0.0: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== /define-properties/1.1.3: dependencies: object-keys: 1.1.1 @@ -866,6 +1063,15 @@ packages: node: '>=6' resolution: integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA== + /duplexify/4.1.1: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.0 + stream-shift: 1.0.1 + dev: false + resolution: + integrity: sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== /dynamic-dedupe/0.3.0: dependencies: xtend: 4.0.2 @@ -880,6 +1086,11 @@ packages: dev: true resolution: integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + /end-of-stream/1.4.4: + dependencies: + once: 1.4.0 + resolution: + integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== /enquirer/2.3.6: dependencies: ansi-colors: 4.1.1 @@ -1178,6 +1389,10 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + /eventemitter3/4.0.4: + dev: false + resolution: + integrity: sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== /fast-deep-equal/3.1.3: dev: true resolution: @@ -1190,8 +1405,13 @@ packages: dev: true resolution: integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - /fast-safe-stringify/2.0.7: + /fast-redact/2.0.0: dev: false + engines: + node: '>=6' + resolution: + integrity: sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA== + /fast-safe-stringify/2.0.7: resolution: integrity: sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== /figures/3.2.0: @@ -1262,6 +1482,10 @@ packages: node: '>=4' resolution: integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + /flatstr/1.0.12: + dev: false + resolution: + integrity: sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== /flatted/2.0.2: dev: true resolution: @@ -1322,6 +1546,14 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + /get-stream/5.1.0: + dependencies: + pump: 3.0.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== /git-raw-commits/2.0.0: dependencies: dargs: 4.1.0 @@ -1387,6 +1619,24 @@ packages: node: '>=8' resolution: integrity: sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + /got/11.5.1: + dependencies: + '@sindresorhus/is': 3.1.0 + '@szmarczak/http-timer': 4.0.5 + '@types/cacheable-request': 6.0.1 + '@types/responselike': 1.0.0 + cacheable-lookup: 5.0.3 + cacheable-request: 7.0.1 + decompress-response: 6.0.0 + http2-wrapper: 1.0.0-beta.5.2 + lowercase-keys: 2.0.0 + p-cancelable: 2.0.0 + responselike: 2.0.0 + dev: false + engines: + node: '>=10.19.0' + resolution: + integrity: sha512-reQEZcEBMTGnujmQ+Wm97mJs/OK6INtO6HmLI+xt3+9CvnRwWjXutUvb2mqr+Ao4Lu05Rx6+udx9sOQAmExMxA== /graceful-fs/4.2.4: dev: true resolution: @@ -1441,6 +1691,19 @@ packages: dev: true resolution: integrity: sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + /http-cache-semantics/4.1.0: + dev: false + resolution: + integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + /http2-wrapper/1.0.0-beta.5.2: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.0.0 + dev: false + engines: + node: '>=10.19.0' + resolution: + integrity: sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== /ignore/4.0.6: dev: true engines: @@ -1496,7 +1759,6 @@ packages: resolution: integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= /inherits/2.0.4: - dev: true resolution: integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== /ini/1.3.5: @@ -1629,6 +1891,18 @@ packages: dev: true resolution: integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + /jmespath/0.15.0: + dev: true + engines: + node: '>= 0.6.0' + resolution: + integrity: sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + /joycon/2.2.5: + dev: true + engines: + node: '>=6' + resolution: + integrity: sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ== /js-tokens/4.0.0: dev: true resolution: @@ -1641,6 +1915,10 @@ packages: hasBin: true resolution: integrity: sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + /json-buffer/3.0.1: + dev: false + resolution: + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== /json-parse-better-errors/1.0.2: dev: true resolution: @@ -1670,12 +1948,24 @@ packages: '0': node >= 0.2.0 resolution: integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + /keyv/4.0.1: + dependencies: + json-buffer: 3.0.1 + dev: false + resolution: + integrity: sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw== /kind-of/6.0.3: dev: true engines: node: '>=0.10.0' resolution: integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + /leven/2.1.0: + dev: true + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-wuep93IJTe6dNCAq6KzORoeHVYA= /levn/0.4.1: dependencies: prelude-ls: 1.2.1 @@ -1753,10 +2043,18 @@ packages: dev: true resolution: integrity: sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + /lodash.camelcase/4.3.0: + dev: false + resolution: + integrity: sha1-soqmKIorn8ZRA1x3EfZathkDMaY= /lodash.ismatch/4.4.0: dev: true resolution: integrity: sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + /lodash.pickby/4.6.0: + dev: false + resolution: + integrity: sha1-feoh2MGNdwOifHBMFdO4SmfjOv8= /lodash.template/4.5.0: dependencies: lodash._reinterpolate: 3.0.0 @@ -1783,8 +2081,23 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + /lowercase-keys/2.0.0: + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + /luxon/1.24.1: + dev: false + resolution: + integrity: sha512-CgnIMKAWT0ghcuWFfCWBnWGOddM0zu6c4wZAWmD0NN7MZTnro0+833DF6tJep+xlxRPg4KtsYEHYLfTMBQKwYg== + /make-error-cause/2.3.0: + dependencies: + make-error: 1.3.6 + dev: false + resolution: + integrity: sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg== /make-error/1.3.6: - dev: true resolution: integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== /map-obj/1.0.1: @@ -1858,6 +2171,18 @@ packages: node: '>=10' resolution: integrity: sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw== + /mimic-response/1.0.1: + dev: false + engines: + node: '>=4' + resolution: + integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + /mimic-response/3.1.0: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== /min-indent/1.0.1: dev: true engines: @@ -1913,8 +2238,13 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== - /ms/2.0.0: + /mri/1.1.4: dev: true + engines: + node: '>=4' + resolution: + integrity: sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== + /ms/2.0.0: resolution: integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= /ms/2.1.2: @@ -1928,6 +2258,12 @@ packages: dev: true resolution: integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + /node-gyp-build/3.7.0: + dev: false + hasBin: true + optional: true + resolution: + integrity: sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w== /noms/0.0.0: dependencies: inherits: 2.0.4 @@ -1950,6 +2286,12 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + /normalize-url/4.5.0: + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== /null-check/1.0.0: dev: true engines: @@ -2003,7 +2345,6 @@ packages: /once/1.4.0: dependencies: wrappy: 1.0.2 - dev: true resolution: integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E= /optionator/0.9.1: @@ -2019,6 +2360,18 @@ packages: node: '>= 0.8.0' resolution: integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + /p-cancelable/2.0.0: + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + /p-finally/1.0.0: + dev: false + engines: + node: '>=4' + resolution: + integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= /p-limit/1.3.0: dependencies: p-try: 1.0.0 @@ -2059,6 +2412,23 @@ packages: node: '>=8' resolution: integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + /p-queue/6.6.0: + dependencies: + eventemitter3: 4.0.4 + p-timeout: 3.2.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-zPHXPNy9jZsiym0PpJjvnHQysx1fSd/QdaNVwiDRLU2KFChD6h9CkCB6b8i3U8lBwJyA+mHgNZCzcy77glUssQ== + /p-timeout/3.2.0: + dependencies: + p-finally: 1.0.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== /p-try/1.0.0: dev: true engines: @@ -2205,6 +2575,39 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + /pino-pretty/4.1.0: + dependencies: + '@hapi/bourne': 2.0.0 + args: 5.0.1 + chalk: 4.1.0 + dateformat: 3.0.3 + fast-safe-stringify: 2.0.7 + jmespath: 0.15.0 + joycon: 2.2.5 + pump: 3.0.0 + readable-stream: 3.6.0 + split2: 3.1.1 + strip-json-comments: 3.1.1 + dev: true + hasBin: true + resolution: + integrity: sha512-D4rORz/7SCZ0AGeWw9QFXsJ+ky3jDOqi/ABFzxxCk5BtEelEgJ6AABFew7TosHGw7I1t8VuMaGdTVNoYMZc+jg== + /pino-std-serializers/2.4.2: + dev: false + resolution: + integrity: sha512-WaL504dO8eGs+vrK+j4BuQQq6GLKeCCcHaMB2ItygzVURcL1CycwNEUHTD/lHFHs/NL5qAz2UKrjYWXKSf4aMQ== + /pino/6.4.1: + dependencies: + fast-redact: 2.0.0 + fast-safe-stringify: 2.0.7 + flatstr: 1.0.12 + pino-std-serializers: 2.4.2 + quick-format-unescaped: 4.0.1 + sonic-boom: 1.0.2 + dev: false + hasBin: true + resolution: + integrity: sha512-1zDSQworQZw14tvqjuW5aj5GV5oUQpV5Bz5wnpVVltVPBzaOoV1Dv+oKn1xNCz2CCkOyZd+kkdlel9lCLBYl+Q== /pkg-dir/2.0.0: dependencies: find-up: 2.1.0 @@ -2229,6 +2632,12 @@ packages: node: '>=0.4.0' resolution: integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + /pump/3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + resolution: + integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== /punycode/2.1.1: dev: true engines: @@ -2242,6 +2651,14 @@ packages: teleport: '>=0.2.0' resolution: integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + /queue-microtask/1.1.4: + dev: false + resolution: + integrity: sha512-eY/4Obve9cE5FK8YvC1cJsm5cr7XvAurul8UtBDJ2PR1p5NmAwHtvAt5ftcLtwYRCUKNhxCneZZlxmUDFoSeKA== + /quick-format-unescaped/4.0.1: + dev: false + resolution: + integrity: sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== /quick-lru/1.1.0: dev: true engines: @@ -2254,6 +2671,25 @@ packages: node: '>=8' resolution: integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + /quick-lru/5.1.1: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + /randombytes/2.1.0: + dependencies: + safe-buffer: 5.2.1 + dev: false + resolution: + integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + /randomstring/1.1.5: + dependencies: + array-uniq: 1.0.2 + dev: false + hasBin: true + resolution: + integrity: sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM= /read-pkg-up/1.0.1: dependencies: find-up: 1.1.2 @@ -2358,7 +2794,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true engines: node: '>= 6' resolution: @@ -2436,6 +2871,10 @@ packages: dev: true resolution: integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + /resolve-alpn/1.0.0: + dev: false + resolution: + integrity: sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== /resolve-from/4.0.0: dev: true engines: @@ -2454,6 +2893,12 @@ packages: dev: true resolution: integrity: sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + /responselike/2.0.0: + dependencies: + lowercase-keys: 2.0.0 + dev: false + resolution: + integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== /rimraf/2.6.3: dependencies: glob: 7.1.6 @@ -2473,9 +2918,14 @@ packages: resolution: integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== /safe-buffer/5.2.1: - dev: true resolution: integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + /semaphore-async-await/1.5.1: + dev: false + engines: + node: '>=4.1' + resolution: + integrity: sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo= /semver/5.7.1: dev: true hasBin: true @@ -2526,6 +2976,16 @@ packages: dev: true resolution: integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + /simple-websocket/9.0.0: + dependencies: + debug: 4.1.1 + queue-microtask: 1.1.4 + randombytes: 2.1.0 + readable-stream: 3.6.0 + ws: 7.3.1 + dev: false + resolution: + integrity: sha512-Q+u1BJ06/FR30xS1Sf6zDuL+vAdAA7VFqZ0TdKpmQKB2uNTAPKWQFFhUDV4YD7TDi7gSRJXoxv21WprNPR0ykQ== /slice-ansi/2.1.0: dependencies: ansi-styles: 3.2.1 @@ -2536,6 +2996,13 @@ packages: node: '>=6' resolution: integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + /sonic-boom/1.0.2: + dependencies: + atomic-sleep: 1.0.0 + flatstr: 1.0.12 + dev: false + resolution: + integrity: sha512-sRMmXu7uFDXoniGvtLHuQk5KWovLWoi6WKASn7rw0ro41mPf0fOolkGp4NE6680CbxvNh26zWNyFQYYWXe33EA== /source-map-support/0.5.19: dependencies: buffer-from: 1.1.1 @@ -2583,6 +3050,11 @@ packages: dev: true resolution: integrity: sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== + /split2/3.1.1: + dependencies: + readable-stream: 3.6.0 + resolution: + integrity: sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q== /sprintf-js/1.0.3: dev: true resolution: @@ -2610,6 +3082,10 @@ packages: hasBin: true resolution: integrity: sha512-L8X9KFq2SmVmaeZgUmWHFJMOsEWpjgFAwqic6yIIoveM1kdw1vH4Io03WWxUDjypjGqGU6qUtcJoR8UvOv5w3g== + /stream-shift/1.0.1: + dev: false + resolution: + integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== /string-width/3.1.0: dependencies: emoji-regex: 7.0.3 @@ -2657,7 +3133,6 @@ packages: /string_decoder/1.3.0: dependencies: safe-buffer: 5.2.1 - dev: true resolution: integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== /stringify-package/1.0.1: @@ -2863,6 +3338,10 @@ packages: typescript: '>=2.7' resolution: integrity: sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== + /ts-toolbelt/6.13.35: + dev: false + resolution: + integrity: sha512-RUlJt2ku8jRCDEZsyCvAYEPIa+99ZpT9YJqccWh5IxDBN09fk2epMrYWiJ4jSFRHNvTx4DqWRP07PSx5QPbflA== /tsconfig-paths/3.9.0: dependencies: '@types/json5': 0.0.29 @@ -2947,8 +3426,15 @@ packages: dev: true resolution: integrity: sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + /utf-8-validate/5.0.2: + dependencies: + node-gyp-build: 3.7.0 + dev: false + optional: true + requiresBuild: true + resolution: + integrity: sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw== /util-deprecate/1.0.2: - dev: true resolution: integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= /v8-compile-cache/2.1.1: @@ -2962,6 +3448,12 @@ packages: dev: true resolution: integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + /validator/13.1.1: + dev: false + engines: + node: '>= 0.10' + resolution: + integrity: sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw== /which-module/2.0.0: dev: true resolution: @@ -2996,7 +3488,6 @@ packages: resolution: integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== /wrappy/1.0.2: - dev: true resolution: integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= /write/1.0.3: @@ -3021,6 +3512,23 @@ packages: optional: true resolution: integrity: sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + /ws/7.3.1_5290a7aab7631971258e1bd11475725e: + dependencies: + bufferutil: 4.0.1 + utf-8-validate: 5.0.2 + dev: false + engines: + node: '>=8.3.0' + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + resolution: + integrity: sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== /xtend/4.0.2: dev: true engines: @@ -3064,22 +3572,51 @@ packages: node: '>=6' resolution: integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - github.com/kararty/BeFriendlier-Shared/f0f3c6f06ac55db28453a337908b816cd8fd4362: + github.com/kararty/BeFriendlier-Shared/d0c50fc044b9d645567d8213258e4f74a48b16b9: + dependencies: + '@adonisjs/logger': 2.1.0 + got: 11.5.1 dev: false name: befriendlier-shared resolution: registry: 'https://registry.npmjs.org/' - tarball: 'https://codeload.github.com/kararty/BeFriendlier-Shared/tar.gz/f0f3c6f06ac55db28453a337908b816cd8fd4362' - version: 4.1.0 + tarball: 'https://codeload.github.com/kararty/BeFriendlier-Shared/tar.gz/d0c50fc044b9d645567d8213258e4f74a48b16b9' + version: 5.4.1 + github.com/kararty/dank-twitch-irc/8dcdac2a6ee0a273a1d0490c59a82bf38e29569c: + dependencies: + '@types/debug': 4.1.5 + '@types/duplexify': 3.6.0 + debug-logger: 0.4.1 + duplexify: 4.1.1 + eventemitter3: 4.0.4 + lodash.camelcase: 4.3.0 + lodash.pickby: 4.6.0 + make-error-cause: 2.3.0 + ms: 2.1.2 + randomstring: 1.1.5 + semaphore-async-await: 1.5.1 + simple-websocket: 9.0.0 + split2: 3.1.1 + ts-toolbelt: 6.13.35 + dev: false + name: dank-twitch-irc + resolution: + registry: 'https://registry.npmjs.org/' + tarball: 'https://codeload.github.com/kararty/dank-twitch-irc/tar.gz/8dcdac2a6ee0a273a1d0490c59a82bf38e29569c' + version: 3.3.0 specifiers: '@adonisjs/env': ^1.0.18 + '@adonisjs/logger': ^2.1.0 + '@adonisjs/validator': ^7.4.0 '@types/node': ^14.0.27 '@types/ws': ^7.2.6 '@typescript-eslint/eslint-plugin': ^3.7.1 '@typescript-eslint/parser': ^3.7.1 adonis-preset-ts: ^1.0.4 - befriendlier-shared: 'github:kararty/BeFriendlier-Shared#semver:^4.1.0' + befriendlier-shared: 'github:kararty/BeFriendlier-Shared#semver:5.4.1' + bufferutil: ^4.0.1 copyfiles: ^2.3.0 + dank-twitch-irc: 'github:kararty/dank-twitch-irc' eslint: ^7.5.0 eslint-config-standard: ^14.1.1 eslint-config-standard-with-typescript: ^18.0.2 @@ -3088,7 +3625,10 @@ specifiers: eslint-plugin-node: ^11.1.0 eslint-plugin-promise: ^4.2.1 eslint-plugin-standard: ^4.0.1 + p-queue: ^6.6.0 + pino-pretty: ^4.1.0 standard-version: ^8.0.2 ts-node-dev: ^1.0.0-pre.56 typescript: ^3.9.7 + utf-8-validate: ^5.0.2 ws: ^7.3.1 diff --git a/src/Handlers/AddEmotesHandler.ts b/src/Handlers/AddEmotesHandler.ts new file mode 100644 index 0000000..9433f72 --- /dev/null +++ b/src/Handlers/AddEmotesHandler.ts @@ -0,0 +1,28 @@ +import { MessageType, ADDEMOTES, BASE } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class AddEmotesHandler extends DefaultHandler { + public messageType = MessageType.ADDEMOTES + + public prefix = ['setemotes'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) as ADDEMOTES + + // TODO: Add FFZ & BTTV emote detections. + + responseMessage.emotes = msg.emotes.map(emote => { + return { + name: emote.code, + id: emote.id, + } + }) + + this.ws.sendMessage(MessageType.ADDEMOTES, JSON.stringify(responseMessage)) + } + + public async onServerResponse ({ channelTwitch, userTwitch, result }: BASE) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, String(result.value)) + } +} diff --git a/src/Handlers/BotHandler.ts b/src/Handlers/BotHandler.ts new file mode 100644 index 0000000..fe7f1af --- /dev/null +++ b/src/Handlers/BotHandler.ts @@ -0,0 +1,16 @@ +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class BotHandler extends DefaultHandler { + // public messageType = MessageType + + public prefix = ['bot'] + + public async onCommand (msg: PrivmsgMessage) { + const message = `${String(this.twitch.packageJSON.description)} Version: ${String(this.twitch.packageJSON.version)}` + + this.twitch.sendMessage(msg.channelName, msg.senderUsername, message) + } + + // public onServerResponse (res) {} +} diff --git a/src/Handlers/ChatsHandler.ts b/src/Handlers/ChatsHandler.ts new file mode 100644 index 0000000..72dc39e --- /dev/null +++ b/src/Handlers/ChatsHandler.ts @@ -0,0 +1,24 @@ +import { MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class ChatsHandler extends DefaultHandler { + public messageType = MessageType.CHATS + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse (requestTime: string) { + const channels = Array.from(this.twitch.channels).map(([_id, channel]) => { + return { id: channel.id, name: channel.name } + }) + + // If requestTime.length > 0, that means this is a requestResponse. + const responseMessage = { + requestTime: requestTime.length > 0 ? requestTime : undefined, + value: channels, + } + + this.ws.sendMessage(MessageType.CHATS, JSON.stringify(responseMessage)) + } +} diff --git a/src/Handlers/DefaultHandler.ts b/src/Handlers/DefaultHandler.ts new file mode 100644 index 0000000..60868d5 --- /dev/null +++ b/src/Handlers/DefaultHandler.ts @@ -0,0 +1,47 @@ +import { Logger } from '@adonisjs/logger/build/standalone' +import { BASE } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import Client from '../Twitch' +import Ws from '../Ws' + +export default class DefaultHandler { + protected readonly twitch: Client + protected readonly ws: Ws + protected readonly logger: Logger + + public messageType = 'DEFAULT' + public prefix = ['*'] + + constructor (twitch: Client, ws: Ws, logger: Logger) { + this.twitch = twitch + this.ws = ws + this.logger = logger + } + + public makeResponseMesage (msg: PrivmsgMessage): BASE { + return { + userTwitch: { + name: msg.senderUsername, + id: msg.senderUserID, + }, + channelTwitch: { + name: msg.channelName, + id: msg.channelID, + }, + } + } + + public async onCommand (_msg?: PrivmsgMessage, _words?: string[]) {} + + public async onServerResponse (_res, _raw?) { + if (_res.data.length > 0) { + const data: BASE = JSON.parse(_res.data) + this.logger.error(`${this.constructor.name}.onServerResponse(): RECEIVED UNHANDLED MESSAGETYPE [${String(_res.type)}]: %O`, _res) + if (data.channelTwitch !== undefined && data.userTwitch !== undefined) { + this.twitch.removeUserInstance(data) + } + } else { + this.logger.error(`${this.constructor.name}.onServerResponse(): RECEIVED UNHANDLED MESSAGE: %O`, _res) + } + } +} diff --git a/src/Handlers/ErrorHandler.ts b/src/Handlers/ErrorHandler.ts new file mode 100644 index 0000000..bd59514 --- /dev/null +++ b/src/Handlers/ErrorHandler.ts @@ -0,0 +1,16 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class ErrorHandler extends DefaultHandler { + public messageType = MessageType.ERROR + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse ({ channelTwitch, userTwitch, result }: BASE) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, result.value) + + this.twitch.removeUserInstance({ userTwitch, channelTwitch }) + } +} diff --git a/src/Handlers/JoinChatHandler.ts b/src/Handlers/JoinChatHandler.ts new file mode 100644 index 0000000..0fd68d6 --- /dev/null +++ b/src/Handlers/JoinChatHandler.ts @@ -0,0 +1,58 @@ +import { JOINCHAT, MessageType } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class JoinChannelHandler extends DefaultHandler { + public messageType = MessageType.JOINCHAT + + public prefix = ['join'] + + public async onCommand (msg: PrivmsgMessage, words: string[]) { + const responseMessage = this.makeResponseMesage(msg) as JOINCHAT + + // Get user details for provided user. + const res = await this.twitch.api.getUser(this.twitch.token.superSecret, [words[1]]) + if (res !== null && res.length > 0) { + responseMessage.joinUserTwitch = { + id: res[0].id, + name: res[0].login, + } + + this.ws.sendMessage(MessageType.JOINCHAT, JSON.stringify(responseMessage)) + } else { + this.twitch.sendMessage(msg.channelName, msg.senderUsername, 'could not find that user on Twitch.') + } + } + + public async onServerResponse ({ /* channelTwitch, userTwitch */ joinUserTwitch }: JOINCHAT) { + const foundExistingChannel = this.twitch.channels.get(joinUserTwitch.name) + + if (foundExistingChannel !== undefined) { + this.logger.warn(`Twitch.joinChannel(): Tried to join a channel already in cached, named ${joinUserTwitch.name}.`) + return + } + + await this.twitch.checkReady() + + return await this.twitch.ircClient.join(joinUserTwitch.name).then(() => { + this.logger.info(`Twitch.JOIN: Joined ${joinUserTwitch.name}.`) + + // if (channelTwitch !== undefined && userTwitch !== undefined) { + // this.twitch.sendMessage( + // joinUserTwitch.name, + // userTwitch.name, + // `from channel @${channelTwitch.name}, has added this channel to the service!` + + // 'BeFriendlier.app for more information.`, + // ) + // } + + this.twitch.joinChannel(joinUserTwitch) + + // Tell server our new channels list. + // eslint-disable-next-line no-void + void this.twitch.handlers.find(command => command.messageType === MessageType.CHATS)?.onCommand() + }).catch(error => { + this.logger.error({ err: error }, 'Twitch.joinChannel() -> Twitch.JOIN') + }) + } +} diff --git a/src/Handlers/LeaveChatHandler.ts b/src/Handlers/LeaveChatHandler.ts new file mode 100644 index 0000000..3b1eb27 --- /dev/null +++ b/src/Handlers/LeaveChatHandler.ts @@ -0,0 +1,38 @@ +import { JOINCHAT, MessageType, LEAVECHAT } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class LeaveChannelHandler extends DefaultHandler { + public messageType = MessageType.LEAVECHAT + + public prefix = ['join'] + + public async onCommand (msg: PrivmsgMessage, words: string[]) { + const responseMessage = this.makeResponseMesage(msg) as JOINCHAT + + // Get user details for provided user. + const res = await this.twitch.api.getUser(this.twitch.token.superSecret, [words[1]]) + if (res !== null && res.length > 0) { + responseMessage.joinUserTwitch = { + id: res[0].id, + name: res[0].login, + } + + this.ws.sendMessage(MessageType.JOINCHAT, JSON.stringify(responseMessage)) + } else { + this.twitch.sendMessage(msg.channelName, msg.senderUsername, 'could not find that user on Twitch.') + } + } + + public async onServerResponse ({ /* channelTwitch, userTwitch, */ leaveUserTwitch }: LEAVECHAT) { + // if (channelTwitch !== undefined && userTwitch !== undefined) { + // this.twitch.sendMessage( + // joinUserTwitch.name, + // userTwitch.name, + // `from channel @${channelTwitch.name}, has issued me to leave this channel. FeelsBadMan Good bye!`, + // ) + // } + + this.twitch.leaveChannel(leaveUserTwitch) + } +} diff --git a/src/Handlers/MatchHandler.ts b/src/Handlers/MatchHandler.ts new file mode 100644 index 0000000..b38ea3e --- /dev/null +++ b/src/Handlers/MatchHandler.ts @@ -0,0 +1,27 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class MatchHandler extends DefaultHandler { + public messageType = MessageType.MATCH + + public prefix = ['match', 'yes'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) + + const foundUserRoll = this.twitch.getUserInstance(msg) + + if (foundUserRoll === undefined) { + return + } + + this.ws.sendMessage(MessageType.MATCH, JSON.stringify(responseMessage)) + } + + public async onServerResponse ({ channelTwitch, userTwitch, result }: BASE) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, String(result.value)) + + this.twitch.removeUserInstance({ channelTwitch, userTwitch }) + } +} diff --git a/src/Handlers/MismatchHandler.ts b/src/Handlers/MismatchHandler.ts new file mode 100644 index 0000000..714ab8b --- /dev/null +++ b/src/Handlers/MismatchHandler.ts @@ -0,0 +1,16 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class MismatchHandler extends DefaultHandler { + public messageType = MessageType.MISMATCH + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse ({ channelTwitch, userTwitch, result }: BASE) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, String(result.value)) + + this.twitch.removeUserInstance({ channelTwitch, userTwitch }) + } +} diff --git a/src/Handlers/MoreHandler.ts b/src/Handlers/MoreHandler.ts new file mode 100644 index 0000000..f1ccdbb --- /dev/null +++ b/src/Handlers/MoreHandler.ts @@ -0,0 +1,26 @@ +import { MessageType, ROLLMATCH } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class MoreHandler extends DefaultHandler { + // public messageType = MessageType + + public prefix = ['more'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) as ROLLMATCH + + const foundUserRoll = this.twitch.getUserInstance(msg) + + if (foundUserRoll === undefined) { + return + } + + foundUserRoll.nextType() + + responseMessage.more = foundUserRoll.type + this.ws.sendMessage(MessageType.ROLLMATCH, JSON.stringify(responseMessage)) + } + + // public async onServerResponse (res) {} +} diff --git a/src/Handlers/NoHandler.ts b/src/Handlers/NoHandler.ts new file mode 100644 index 0000000..01cf822 --- /dev/null +++ b/src/Handlers/NoHandler.ts @@ -0,0 +1,23 @@ +import { MessageType } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class NoHandler extends DefaultHandler { + // public messageType = MessageType + + public prefix = ['no', 'mismatch'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) + + const foundUserRoll = this.twitch.getUserInstance(msg) + + if (foundUserRoll === undefined) { + return + } + + this.ws.sendMessage(MessageType.MISMATCH, JSON.stringify(responseMessage)) + } + + // public async onServerResponse (res) {} +} diff --git a/src/Handlers/PingHandler.ts b/src/Handlers/PingHandler.ts new file mode 100644 index 0000000..316d000 --- /dev/null +++ b/src/Handlers/PingHandler.ts @@ -0,0 +1,32 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class PingHandler extends DefaultHandler { + public messageType = MessageType.PING + + public prefix = ['ping'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) + + const dateNow = Date.now() + await this.twitch.ircClient.ping() + const dateAfterPing = Date.now() + responseMessage.result = { pingFromBotToTwitch: dateAfterPing - dateNow } + this.ws.sendMessage(MessageType.PING, JSON.stringify(responseMessage)) + } + + public async onServerResponse (data: BASE | undefined, res: { timestamp: number }) { + if (data !== undefined) { + this.twitch.sendMessage( + data.channelTwitch.name, + data.userTwitch.name, + `ping from Bot to Twitch: ~${String(data.result.pingFromBotToTwitch)} ms. ` + + `Ping from Bot to Website roundabout: ~${String(Date.now() - res.timestamp)} ms.`, + ) + } else { + // TODO: Looks like this is a healthcheck! + } + } +} diff --git a/src/Handlers/RollMatchHandler.ts b/src/Handlers/RollMatchHandler.ts new file mode 100644 index 0000000..0a890c6 --- /dev/null +++ b/src/Handlers/RollMatchHandler.ts @@ -0,0 +1,29 @@ +import { MessageType, ROLLMATCH } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class RollMatchHandler extends DefaultHandler { + public messageType = MessageType.ROLLMATCH + + public prefix = ['swipe', 'roll'] + + public async onCommand (msg: PrivmsgMessage) { + const responseMessage = this.makeResponseMesage(msg) as ROLLMATCH + + let foundUserRoll = this.twitch.getUserInstance(msg) + + if (foundUserRoll !== undefined) { + return + } + + foundUserRoll = this.twitch.createAndGetUserInstance(msg) + + responseMessage.more = foundUserRoll.type + + this.ws.sendMessage(MessageType.ROLLMATCH, JSON.stringify(responseMessage)) + } + + public async onServerResponse ({ channelTwitch, userTwitch, result }: ROLLMATCH) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, String(result.value)) + } +} diff --git a/src/Handlers/SuccessHandler.ts b/src/Handlers/SuccessHandler.ts new file mode 100644 index 0000000..742c12b --- /dev/null +++ b/src/Handlers/SuccessHandler.ts @@ -0,0 +1,28 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class SuccessHandler extends DefaultHandler { + public messageType = MessageType.SUCCESS + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse ({ channelTwitch, userTwitch, result }: BASE) { + // Send to this user. + this.twitch./* TODO: whisper */sendMessage( + /** TODO. REMOVE */channelTwitch.name, + userTwitch.name, + String(result.value).replace('%s', `@${String(result.matchUsername)}`), + ) + + // // Send to matched user. + // this.twitch./* TODO: whisper */sendMessage( + // /** TODO. REMOVE */channelTwitch.name, + // result.matchUsername, + // String(result.value).replace('%s', `@${String(userTwitch.name)}`), + // ) + + this.twitch.removeUserInstance({ channelTwitch, userTwitch }) + } +} diff --git a/src/Handlers/TakeABreakHandler.ts b/src/Handlers/TakeABreakHandler.ts new file mode 100644 index 0000000..de394d7 --- /dev/null +++ b/src/Handlers/TakeABreakHandler.ts @@ -0,0 +1,20 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class TakeABreakHandler extends DefaultHandler { + public messageType = MessageType.TAKEABREAK + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse ({ channelTwitch, userTwitch }: BASE) { + this.twitch.sendMessage( + channelTwitch.name, + userTwitch.name, + 'take a break! You\'re currently on a cooldown period.', + ) + + this.twitch.removeUserInstance({ channelTwitch, userTwitch }) + } +} diff --git a/src/Handlers/TokenHandler.ts b/src/Handlers/TokenHandler.ts new file mode 100644 index 0000000..ba3044e --- /dev/null +++ b/src/Handlers/TokenHandler.ts @@ -0,0 +1,17 @@ +import { MessageType, Token } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class TokenHandler extends DefaultHandler { + public messageType = MessageType.TOKEN + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse (res: Token) { + this.twitch.token = res + + // Login / Relogin to Twitch + return await this.twitch.loginToTwitch() + } +} diff --git a/src/Handlers/UnmatchHandler.ts b/src/Handlers/UnmatchHandler.ts new file mode 100644 index 0000000..f675e23 --- /dev/null +++ b/src/Handlers/UnmatchHandler.ts @@ -0,0 +1,30 @@ +import { UNMATCH, MessageType } from 'befriendlier-shared' +import { PrivmsgMessage } from 'dank-twitch-irc' +import DefaultHandler from './DefaultHandler' + +export default class UnmatchHandler extends DefaultHandler { + public messageType = MessageType.UNMATCH + + public prefix = ['unmatch'] + + public async onCommand (msg: PrivmsgMessage, words: string[]) { + const responseMessage = this.makeResponseMesage(msg) as UNMATCH + + // Get user details for provided user. + const res = await this.twitch.api.getUser(this.twitch.token.superSecret, [words[1]]) + if (res !== null && res.length > 0) { + responseMessage.matchUserTwitch = { + id: res[0].id, + name: res[0].login, + } + + this.ws.sendMessage(MessageType.UNMATCH, JSON.stringify(responseMessage)) + } else { + this.twitch.sendMessage(msg.channelName, msg.senderUsername, 'could not find that user on Twitch.') + } + } + + public async onServerResponse ({ channelTwitch, userTwitch, result }: UNMATCH) { + this.twitch.sendMessage(channelTwitch.name, userTwitch.name, String(result.value)) + } +} diff --git a/src/Handlers/UnregisteredHandler.ts b/src/Handlers/UnregisteredHandler.ts new file mode 100644 index 0000000..711dca9 --- /dev/null +++ b/src/Handlers/UnregisteredHandler.ts @@ -0,0 +1,20 @@ +import { BASE, MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class UnregisteredHandler extends DefaultHandler { + public messageType = MessageType.UNREGISTERED + + // public prefix = [''] + + // public async onCommand (msg: PrivmsgMessage) {} + + public async onServerResponse ({ channelTwitch, userTwitch }: BASE) { + this.twitch.sendMessage( + channelTwitch.name, + userTwitch.name, + 'you\'re not registered! Browse to the website to register.', + ) + + this.twitch.removeUserInstance({ channelTwitch, userTwitch }) + } +} diff --git a/src/Handlers/WelcomeHandler.ts b/src/Handlers/WelcomeHandler.ts new file mode 100644 index 0000000..ed6495a --- /dev/null +++ b/src/Handlers/WelcomeHandler.ts @@ -0,0 +1,16 @@ +import { MessageType } from 'befriendlier-shared' +import DefaultHandler from './DefaultHandler' + +export default class WelcomeHandler extends DefaultHandler { + public messageType = MessageType.WELCOME + + // public prefix = [''] + + public async onCommand () { + this.ws.sendMessage(MessageType.WELCOME, '') + } + + public async onServerResponse () { + await this.onCommand() + } +} diff --git a/src/Twitch.ts b/src/Twitch.ts new file mode 100644 index 0000000..04b21cd --- /dev/null +++ b/src/Twitch.ts @@ -0,0 +1,312 @@ +/* eslint-disable no-void */ +import { Logger } from '@adonisjs/logger/build/standalone' +import { BASE, MessageType, More, NameAndId, TwitchAuth } from 'befriendlier-shared' +import { + AlternateMessageModifier, + ChatClient, + ClearchatMessage, + ClearmsgMessage, + ConnectionError, + PrivmsgMessage, + PrivmsgMessageRateLimiter, + SlowModeRateLimiter, +} from 'dank-twitch-irc' +import PQueue from 'p-queue' +import TwitchConfig from '../config/Twitch' +import DefaultHandler from './Handlers/DefaultHandler' +import Ws, { WsRes } from './Ws' + +function escapeRegExp (text: string) { + return text.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') +} + +interface Token { + expiration: Date + superSecret: string + refreshToken: string +} + +export interface Channel { + id: string + name: string + cooldown: Date + userRolls: Map +} + +class Message { + public readonly msg: PrivmsgMessage + public deleted = false + public timer: NodeJS.Timeout + + constructor (msg: PrivmsgMessage, clientRef) { + this.msg = msg + + this.timer = setTimeout(() => { + clientRef.onMessage(this) + }, 1000) + } +} + +export class RollInstance { + public type: More + + constructor () { + this.type = More.NONE + } + + public nextType () { + switch (this.type) { + case More.NONE: + this.type = More.BIO + break + case More.BIO: + this.type = More.FAVORITEEMOTES + break + case More.FAVORITEEMOTES: + this.type = More.FAVORITESTREAMERS + break + case More.FAVORITESTREAMERS: + this.type = More.BIO + break + } + } +} + +export default class Client { + private readonly ws: Ws + private readonly logger: Logger + + private readonly name: string + + private readonly commandPrefix: string + + private reconnectAttempts: number = 0 + + private ready = false + + private readonly generalQueue = new PQueue({ concurrency: 1 }) + + public readonly api: TwitchAuth + public readonly packageJSON + public token: Token + + public ircClient: ChatClient + + public readonly msgs: Map = new Map() + public readonly channels: Map = new Map() + + public readonly handlers: DefaultHandler[] = [] + + constructor (config: TwitchConfig, ws: Ws, api: TwitchAuth, packageJSON, logger: Logger) { + this.api = api + this.ws = ws + + this.packageJSON = packageJSON + + this.logger = logger + + this.name = config.user.name + + this.commandPrefix = config.commandPrefix + + this.ws.eventEmitter.on('WS.MESSAGE', (data: WsRes) => this.onServerResponse(data)) + this.ws.eventEmitter.on('WS.CLOSED', (data) => this.onServerClosed(data)) + } + + public async onMessage ({ msg, deleted }: Message) { + if (deleted) { + return + } + + // Filter bad words. + // TODO: Add a global filter. + if (msg.flags instanceof Array) { + for (let index = 0; index < msg.flags.length; index++) { + const word = msg.flags[index].word + const censorStars = Array(word.length).fill('*').join('') + msg.messageText.replace(new RegExp(escapeRegExp(word)), censorStars) + } + } + + const words = msg.messageText.substring(this.commandPrefix.length).split(' ') + + void this.generalQueue?.add(async () => + await this.handlers.find(command => command.prefix.includes(words[0].toLowerCase()))?.onCommand(msg, words)) + } + + public sendMessage (channelName: string, username: string, message: string) { + this.ircClient.say(channelName, `@${username}, ${message}`) + .catch(error => this.logger.error({ err: error }, 'Twitch.sendMessage()')) + } + + public joinChannel ({ id, name }: NameAndId) { + this.channels.set(id, { + id, + name: name, + cooldown: new Date(), + userRolls: new Map(), + }) + } + + public leaveChannel ({ id, name }: NameAndId) { + this.channels.delete(id) + + this.ircClient.part(name).then(() => this.logger.info(`Twitch.leaveChannel() -> Twitch.PART: ${name}`)).catch( + error => this.logger.error({ err: error }, 'Twitch.leaveChannel() -> Twitch.PART')) + } + + public createAndGetUserInstance (msg: PrivmsgMessage) { + this.channels.get(msg.channelID)?.userRolls.set(msg.senderUserID, new RollInstance()) + return this.getUserInstance(msg) as RollInstance + } + + public getUserInstance (msg: PrivmsgMessage) { + return this.channels.get(msg.channelID)?.userRolls.get(msg.senderUserID) + } + + public removeUserInstance ({ channelTwitch, userTwitch }: BASE) { + const userInstance = this.channels.get(channelTwitch.id)?.userRolls.get(userTwitch.id) + + if (userInstance !== undefined) { + this.channels.get(channelTwitch.id)?.userRolls.delete(userTwitch.id) + } + } + + private onServerResponse (res: WsRes) { + const command = this.handlers.find(command => command.messageType === res.type) + + if (command === undefined || res.data === undefined) { + void this.generalQueue.add(async () => + await this.handlers.find(command => command.messageType === 'DEFAULT')?.onServerResponse(res), + { priority: Date.now() }) + + return + } + + const data = JSON.parse(res.data) + + void this.generalQueue.add(async () => await command.onServerResponse(data, res), { priority: Date.now() }) + } + + public async loginToTwitch () { + // We clean off all events, just for precautionary measures. + if (this.ircClient !== undefined) { + this.ircClient.removeAllListeners() + this.ircClient.close() + ;(this.ircClient as any) = undefined + } + + // Relogin to chat! + this.ircClient = new ChatClient({ + password: `oauth:${this.token.superSecret}`, + rateLimits: 'default', + username: this.name, + connection: { + type: 'websocket', + secure: true, + }, + }) + + this.ircClient.use(new AlternateMessageModifier(this.ircClient)) + this.ircClient.use(new SlowModeRateLimiter(this.ircClient)) + this.ircClient.use(new PrivmsgMessageRateLimiter(this.ircClient)) + + // Re-add/Add Twitch socket events + this.ircClient.on('372', (msg) => { + this.logger.info(`Twitch.372: ${msg.ircParameters.join(' ')}`) + }) + + this.ircClient.once('ready', () => { + this.logger.info('Twitch.READY: Successfully connected to Twitch IRC.') + this.ready = true + }) + this.ircClient.on('error', (error) => this.onError(error)) + + // All messages are delayed by 1000ms for time-out checking. + this.ircClient.on('PRIVMSG', (msg) => { + if (!msg.messageText.startsWith(this.commandPrefix)) { + return + } + + const foundChannel = this.channels.get(msg.channelID) + + if (foundChannel === undefined) { + this.leaveChannel({ id: msg.channelID, name: msg.channelName }) + return + } else if (foundChannel.cooldown.getTime() > Date.now()) { + return + } + + foundChannel.cooldown = new Date(Date.now() + 5000) + + // Message class has a "clientRef" to "this" so it can call clientRef.onMessage(). + this.msgs.set(msg.messageID, new Message(msg, this)) + }) + + this.ircClient.on('CLEARCHAT', (msg) => this.deleteMessage(msg)) + this.ircClient.on('CLEARMSG', (msg) => this.deleteMessage(msg)) + + // Finally, connect to Twitch IRC. + return this.ircClient.connect() + } + + public async checkReady () { + if (!this.ready) { + await new Promise(resolve => setTimeout(resolve, 500)) + this.generalQueue.concurrency++ + return await this.checkReady() + } else { + this.generalQueue.concurrency = 1 + return await Promise.resolve() + } + } + + private onServerClosed (data: { type: MessageType, data: string, state: 0 | 2 | 3 }) { + const responseMessage = data.state === this.ws.client.CONNECTING + ? 'Please wait, service is currently in the process of starting. Try again in a bit!' + : data.state === this.ws.client.CLOSING + ? 'service is currently shutting down. Check the website for status updates!' + : 'service is currently down! Check the website for status updates!' + + const res: BASE = JSON.parse(data.data) + + this.sendMessage(res.channelTwitch.name, res.userTwitch.name, responseMessage) + + this.removeUserInstance(res) + } + + private deleteMessage (msg: ClearchatMessage | ClearmsgMessage) { + for (const [, cachedMsg] of this.msgs) { + const removeMsgBool = msg instanceof ClearchatMessage + ? cachedMsg.msg.channelName === msg.channelName + : msg instanceof ClearmsgMessage + ? cachedMsg.msg.messageID === msg.targetMessageID + : false + + if (removeMsgBool) { + cachedMsg.deleted = true + clearTimeout(cachedMsg.timer) + } + } + } + + private onError (error: Error) { + this.logger.error({ err: error }, 'Twitch.onError()') + + if (error instanceof ConnectionError) { + this.reconnectAttempts++ + + const timeSeconds = this.reconnectAttempts > 50 ? 60 : (this.reconnectAttempts + 9) + + this.logger.info(`TWITCH.CLOSE: ATTEMPT #${this.reconnectAttempts}. Reconnecting in ${timeSeconds} seconds...`) + + this.channels.clear() + + this.ready = false + + setTimeout(() => { + void this.loginToTwitch() + }, timeSeconds * 1000) + } + } +} diff --git a/src/Ws.ts b/src/Ws.ts index b4ca42b..a1f800c 100644 --- a/src/Ws.ts +++ b/src/Ws.ts @@ -1,31 +1,126 @@ -// import { MessageType } from 'befriendlier-shared' +import { Logger } from '@adonisjs/logger/build/standalone' +import { schema } from '@adonisjs/validator/build/src/Schema' +import { validator } from '@adonisjs/validator/build/src/Validator' +import { MessageType } from 'befriendlier-shared' +import { EventEmitter } from 'events' import WS from 'ws' +import WSConfig from '../config/Ws' + +export interface WsRes { + type: MessageType + data: string | undefined + timestamp: number +} export default class Bot { - private readonly client: WS + public client: WS + + private readonly logger: Logger + private readonly url: string + private readonly headers: { 'user-agent': string } + + public eventEmitter: EventEmitter - constructor (url: string, headers: { [key: string]: string }) { - this.client = new WS(url, { headers }) + private reconnectAttempts: number = 0 + + constructor (config: WSConfig, logger: Logger) { + this.url = config.url + this.headers = config.headers + this.logger = logger + + this.eventEmitter = new EventEmitter() + } + + public connect () { + if (this.client !== undefined) { + this.client.removeAllListeners() + } + + this.client = new WS(this.url, { headers: this.headers }) this.client.on('open', () => this.onOpen()) this.client.on('message', (data) => this.onMessage(data)) this.client.on('close', (code, reason) => this.onClose(code, reason)) this.client.on('ping', (data) => this.onPing(data)) + this.client.on('error', (err) => this.onError(err)) + } + + public sendMessage (type: MessageType, data: string) { + if (this.client.readyState === 0 || this.client.readyState > 1) { + this.eventEmitter.emit('WS.CLOSED', { type, data, state: this.client.readyState }) + return + } + + this.client.send(this.socketMessage(type, data)) } private onOpen () { - console.log('CONNECTED: %s', this.client.url) + this.logger.info(`Ws.onOpen() ${prettySocketInfo(this.url)}`) + this.eventEmitter.emit('WS.OPEN') + this.reconnectAttempts = 0 } private onMessage (data: WS.Data) { - console.log(data) + this.logger.debug(`Ws.onMessage() ${prettySocketInfo(this.url)}: %O`, data) + + let json + + try { + json = JSON.parse(data as string) + } catch (error) { + this.logger.error({ err: error }, 'Ws.onMessage(): Error with parsing websocket data.') + // Data's not JSON. + return + } + + // LOG ERROR ON DATA THAT'S NOT PROPERLY FORMATTED + validator.validate({ + schema: this.validationSchema, + data: json, + messages: { + type: 'Invalid type.', + }, + cacheKey: 'websocket', + }).then(async (res: WsRes) => { + this.eventEmitter.emit('WS.MESSAGE', res) + }).catch((error: Error) => { + this.logger.error({ err: error }, 'Ws.onMessage()') + }) } private onClose (code: number, reason: string) { - console.error('CLOSE: code: %s, reason:\n%s', code, reason) + this.logger.error(`Ws.onClose() ${prettySocketInfo(this.url)}: code: ${code}${reason.length > 0 ? `, reason:\n${reason}` : ''}`) + + this.reconnectAttempts++ + + const timeSeconds = this.reconnectAttempts > 50 ? 60 : (this.reconnectAttempts + 9) + + this.logger.info(`Ws.onClose() ${prettySocketInfo(this.url)}: ATTEMPT #${this.reconnectAttempts}. Reconnecting in ${timeSeconds} seconds...`) + + setTimeout(() => { + this.connect() + }, timeSeconds * 1000) } private onPing (data: Buffer) { - console.log(`PING${data.length > 0 ? `: ${data.toString()}` : '!'}`) + this.logger.debug(`Ws.onPing() ${prettySocketInfo(this.url)}${data.length > 0 ? `: ${data.toString()}` : ''}`) } + + private onError (error) { + this.logger.error({ err: error }, `Ws.onError() ${prettySocketInfo(this.url)}`) + } + + private socketMessage (type: MessageType, data: string) { + return JSON.stringify({ type: type, data: data, timestamp: Date.now() }) + } + + private readonly validationSchema = schema.create({ + type: schema.enum(Object.values(MessageType)), + data: schema.string.optional(), + timestamp: schema.number(), + }) +} + +function prettySocketInfo (url: string) { + return `[${url}]` } diff --git a/tsconfig.json b/tsconfig.json index 32075fa..ee9199a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "incremental": true, "rootDir": ".", "sourceMap": true, + "resolveJsonModule": true, "types": [ "node" ]