const {extend: createGotClient} = require('got'); const {ConnectionError, SayError, TimeoutError, ChatClient} = require('dank-twitch-irc'); const chalk = require('chalk'); const WS = require('ws'); const Config = require('./config.json'); const clients = []; const banChunks = []; const pingInterval = 120e3; 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', throwHttpErrors: false, headers: { 'Authorization': `Bearer ${Config.PNSLToken}`, }, }); const fetchList = async (list) => { const listID = uuidRegex.exec(list)?.[0]; if (!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 {body: 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; } 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)); }; const createTwitchClient = () => { return new ChatClient({ username: Config.Username, password: Config.Password, rateLimits: 'verifiedBot', }); }; const getConnection = () => { const readyClients = clients.filter((i) => i.ready); return readyClients[++lastIndex % readyClients.length]; }; const say = (channel, message) => { getConnection().say(channel, message); }; 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 () => { await clients[i].join(Config.Username); }); 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}`); } if (error instanceof TimeoutError) { clients[i].connect(); return console.error(`Timeout in client ${i} -> ${error.name} || ${error.message} | Trying to reconnect`); } console.error(error); }); return await new Promise((resolve) => clients[i].on('ready', () => resolve())); })); 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 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 say(Config.Username, 'No list ID provided!'); try { if (channel) { const mods = await getConnection().getMods(channel); if (!mods.includes(Config.Username)) return say(Config.Username, `I am not a moderator in channel ${channel}!`); } const data = await fetchList(listID); await runList(data.listData, listID, channel || channelName); say(Config.Username, `List ${listID} executed successfully on channel ${channel || channelName}`); } catch (error) { console.error(error); return say(Config.Username, `Error Occurred! ${error.message}`); } } }); setInterval(() => { for (const client of clients) { client.ping(); } }, pingInterval); })();