diff --git a/config_dist.json b/config_dist.json index 3b03cc4..49fef98 100644 --- a/config_dist.json +++ b/config_dist.json @@ -2,7 +2,8 @@ "Username": "", "Password": "", "PNSLToken": "", - "AdminID": "", + "Users": [], + "Prefix": "!!", "MaxConnections": 30, "MaxChunkSize": 2000, "DelayPerChunk": 30e3 diff --git a/index.js b/index.js index 491ee78..e1f3885 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,15 @@ const {extend: createGotClient} = require('got'); -const {ChatClient} = require('dank-twitch-irc'); +const {ConnectionError, SayError, ChatClient} = require('dank-twitch-irc'); const chalk = require('chalk'); +const WS = require('ws'); const Config = require('./config.json'); const clients = []; -let banChunks = []; +const banChunks = []; +const pingInterval = 60e3; let lastIndex = 0; const uuidRegex = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; +const msgRegex = /[\u034f\u2800\u{E0000}\u180e\ufeff\u2000-\u200d\u206D]/gu; const pnslClient = createGotClient({ prefixUrl: 'https://bot.tetyys.com/api/v1', @@ -16,6 +19,71 @@ const pnslClient = createGotClient({ }, }); +const fetchList = async (listID) => { + if (!uuidRegex.test(listID)) { + throw new Error('Invalid List ID!'); + } + + const {body: listMetaBody, statusCode} = await pnslClient('BotLists/Properties', {searchParams: {Guid: listID}}); + if (statusCode === 404) { + throw new Error('List was not found.'); + } + + if (statusCode === 401) { + throw new Error('Authorization token is invalid'); + } + + if (statusCode !== 200) { + throw new Error('Unexpected error occurred!'); + } + + const listData = await pnslClient(`BotLists/${listID}`); + + const listMeta = JSON.parse(listMetaBody); + return {listMeta, listData}; +}; + +const runList = async (listData, listID, channel) => { + const banArr = listData.split('\n'); + + if (banArr.length > Config.MaxChunkSize) { + while (banArr.length > 0) { + banChunks.push(banArr.splice(0, Config.MaxChunkSize)); + } + + for (const [i, chunk] of banChunks.entries()) { + const promises = []; + + for (const entry of chunk) { + if (entry === '') continue; + [, user, ...reason] = entry.split(' '); + promises.push(getConnection().ban(channel, user, reason.join(' '))); + } + + await Promise.allSettled(promises); + + banChunks.shift(); + + if (banChunks.length === 0) { + break; + } + + clients[0].say(Config.Username, `Chunk ${Number(i) + 1} of ${listID} executed successfully on channel ${channel}`); + await sleep(Config.DelayPerChunk); + } + } else { + const promises = []; + + for (const entry of banArr) { + if (entry === '') continue; + [, user, ...reason] = entry.split(' '); + promises.push(getConnection().ban(channel, user, reason.join(' '))); + } + + await Promise.allSettled(promises); + } +}; + const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; @@ -28,70 +96,100 @@ const createTwitchClient = () => { }); }; -const sendPong = () => { - for (const [i, client] of Object.entries(clients)) { - client.say(Config.Username, `Pong from client ${Number(i) + 1}`); - } -}; - const getConnection = () => { - return clients[++lastIndex % clients.length]; + const readyClients = clients.filter((i) => i.ready); + return readyClients[++lastIndex % readyClients.length]; }; -(() => { - [...Array(Config.MaxConnections)].map((_, i) => { +const pnslWebSocket = new WS('wss://bot.tetyys.com/api/wss', [], {headers: {Authorization: `Bearer ${Config.PNSLToken}`}}); + +pnslWebSocket.on('open', () => { + console.log(`${chalk.green('[P&SL]')} || Connected to P&SL Websocket server`); +}); + +pnslWebSocket.on('message', async (data) => { + const {o: type, p: payload} = JSON.parse(data); + switch (type) { + case 0: { + const {listMeta} = await fetchList(payload.LatestBotList); + console.log(`${chalk.green('[P&SL]')} || Latest Botlist: ${payload.LatestBotList} [${listMeta.name}] (${listMeta.count})`); + break; + } + case 1: { + console.log(`${chalk.green('[P&SL]')} || New Botlist: ${payload.Guid} [${payload.Name}] (${payload.Count}) (${payload.Tags.join(' ')})`); + break; + } + case 2: { + console.log(`${chalk.green('[P&SL]')} || New False Positive: ${payload.UserTwitchId} [${payload.BotListGuid}]`); + break; + } + + default: + break; + } +}); + +(async () => { + await Promise.all([...Array(Config.MaxConnections)].map(async (_, i) => { clients[i] = createTwitchClient(); clients[i].connect(); clients[i].on('ready', async () => { - console.log(`${chalk.green('[CONNECTED]')} || Client ${Number(i) + 1} Connected to twitch.`); await clients[i].join(Config.Username); }); - }); - - clients[0].on('PRIVMSG', async (msg) => { - if (msg.messageText === '=mbping' && msg.senderUserID === Config.AdminID) { - sendPong(); - } else if (msg.messageText.startsWith('=mbrun') && msg.senderUserID === Config.AdminID) { - const listID = msg.messageText.split(' ')[1]; - const channel = msg.messageText.split(' ')[2]; - if (!uuidRegex.test(listID)) { - return clients[0].say(msg.channelName, 'Invalid List ID!'); - } - const {body, statusCode} = await pnslClient(`BotLists/${listID}`); - - if (statusCode === 404) { - return clients[0].say(msg.channelName, 'List was not found.'); - } - - if (statusCode === 401) { - return clients[0].say(msg.channelName, 'Authorization token is invalid'); - } - - if (statusCode !== 200) { - return clients[0].say(msg.channelName, 'Unexpected error occurred.'); - } - - const banArr = body.split('\n'); - - if (banArr.length > Config.MaxChunkSize) { - while (banArr.length > 0) { - banChunks.push(banArr.splice(0, Config.MaxChunkSize)); + clients[i].on('error', (error) => { + if (error instanceof SayError) return; + if (error instanceof ConnectionError) { + if (clients[i].ready) { + return; } + clients[i].connect(); + return console.error(`Error in client ${i} -> ${error.name} || ${error.message}`); + } + console.error(error); + }); + return await new Promise((resolve) => clients[i].on('ready', () => resolve())); + })); - for (const chunk of banChunks) { - for (const entry of chunk) { - getConnection().privmsg(channel || msg.channelName, entry); - } - await sleep(Config.DelayPerChunk); + console.log(`${chalk.green('[TWITCH]')} || ${clients.filter((i) => i.ready).length} Clients connected`); + + clients[0].on('PRIVMSG', async ({messageText, senderUserID, channelName}) => { + if (!Config.Users.includes(senderUserID)) return; + + const message = messageText.replace(msgRegex, '').trimRight(); + if (!message.startsWith(Config.Prefix)) return; + const content = message.split(/\s+/g); + const command = content[0].slice(Config.Prefix.length); + const args = content.slice(1); + + if (command === 'ping') { + const channel = args[0]; + return clients[0].say(channel || channelName, `${clients.filter((i) => i.ready).length} Clients from total of ${clients.length} are operational`); + } + + if (command === 'runlist') { + const listID = args[0]; + const channel = args[1]; + + if (!listID) return clients[0].say(Config.Username, 'No list ID provided!'); + + try { + if (channel) { + const mods = await clients[0].getMods(channel); + if (!mods.includes(Config.Username)) return clients[0].say(Config.Username, `I am not a moderator in channel ${channel}!`); } - clients[0].say(msg.channelName, `List ${listID} ran successfully on channel ${channel || msg.channelName}`); - banChunks = []; - } else { - for (const entry of banArr) { - getConnection().privmsg(channel || msg.channelName, entry); - } - clients[0].say(msg.channelName, `List ${listID} ran successfully on channel ${channel || msg.channelName}`); + const data = await fetchList(listID); + await runList(data.listData, listID, channel || channelName); + clients[0].say(Config.Username, `List ${listID} executed successfully on channel ${channel}`); + } catch (error) { + console.error(error); + return clients[0].say(Config.Username, `Error Occurred! ${error.message}`); } } }); + + setInterval(() => { + for (const client of clients) { + client.ping(); + } + }, pingInterval); })(); diff --git a/package.json b/package.json index 1d27f67..8aed1b2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "license": "MIT", "dependencies": { "chalk": "^4.1.0", - "dank-twitch-irc": "^4.2.0", - "got": "^11.8.1" + "dank-twitch-irc": "^4.3.0", + "got": "^11.8.1", + "ws": "^7.4.1" }, "devDependencies": { "@yarnpkg/pnpify": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 1a4e81e..468d380 100644 --- a/yarn.lock +++ b/yarn.lock @@ -667,9 +667,9 @@ __metadata: languageName: node linkType: hard -"dank-twitch-irc@npm:^4.2.0": - version: 4.2.0 - resolution: "dank-twitch-irc@npm:4.2.0" +"dank-twitch-irc@npm:^4.3.0": + version: 4.3.0 + resolution: "dank-twitch-irc@npm:4.3.0" dependencies: "@types/debug": ^4.1.5 "@types/duplexify": ^3.6.0 @@ -679,13 +679,13 @@ __metadata: lodash.camelcase: ^4.3.0 lodash.pickby: ^4.6.0 make-error-cause: ^2.3.0 - ms: ^2.1.2 + ms: ^2.1.3 randomstring: ^1.1.5 semaphore-async-await: ^1.5.1 simple-websocket: ^9.0.0 split2: ^3.2.1 ts-toolbelt: ^8.0.3 - checksum: 2f8e2df4b52a10977fcab0241583f0c0619aabf6e1e5988d8f6e9e4feed397b866981b359aefae1f50d279e6c9a05d8a28316c9412c20b6bf0fd220a36667814 + checksum: b3310781c72ba2d2efc65adc8a4ead17c1736eb10859d67fdc2123bd1194c1a23f94c4c27f6aaa33929b1577320f6d07726288d18cf535f7933d2f5e3377d487 languageName: node linkType: hard @@ -1475,10 +1475,11 @@ __metadata: dependencies: "@yarnpkg/pnpify": ^2.4.0 chalk: ^4.1.0 - dank-twitch-irc: ^4.2.0 + dank-twitch-irc: ^4.3.0 eslint: ^7.15.0 eslint-config-google: ^0.14.0 got: ^11.8.1 + ws: ^7.4.1 languageName: unknown linkType: soft @@ -1554,7 +1555,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.2": +"ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 6e721e648a544154d5de4c114b32f573d8027ca8ec505cf6c1105e505986d6ac46934a1256735aa0eece8eb2f5b2a1230503b2dddd3b100f9f016fd8a4f15f33 @@ -2201,7 +2202,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.0.0": +"ws@npm:^7.0.0, ws@npm:^7.4.1": version: 7.4.1 resolution: "ws@npm:7.4.1" peerDependencies: