const { extend: createGotClient } = require('got'); const { ConnectionError, SayError, TimeoutError, UserBanError, ChatClient } = require('dank-twitch-irc'); const chalk = require('chalk'); const WS = require('ws'); const Config = require('./config.json'); const clients = []; 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 urlClient = createGotClient(); 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 fetchUrlList = async (list) => { const { body: listMetaBody, statusCode } = await urlClient(list); if (statusCode === 404) { throw new Error('List was not found.'); } if (statusCode !== 200) { throw new Error('Unexpected error occured!'); } return listMetaBody; }; const runList = async (listData, listID, channel) => { const banArr = listData.split('\n'); if (banArr.length > Config.MaxChunkSize) { const banChunks = []; while (banArr.length > 0) { banChunks.push(banArr.splice(0, Config.MaxChunkSize)); } const chunkCount = banChunks.length; for (const [i, chunk] of banChunks.entries()) { const currentChunk = i + 1; const promises = []; for (const entry of chunk) { let user = ''; let reason = ''; if (entry === '') { continue; } [, user, ...reason] = entry.split(' '); promises.push(getConnection().ban(channel, user, reason.join(' '))); } await Promise.allSettled(promises); say(Config.Username, `Chunk ${currentChunk}/${chunkCount} of ${listID} executed successfully on channel ${channel}`); if (currentChunk === chunkCount) { break; } await sleep(Config.DelayPerChunk); } } else { const promises = []; for (const entry of banArr) { let user = ''; let reason = ''; if (entry === '') { continue; } [, user, ...reason] = entry.split(' '); promises.push(getConnection().ban(channel, user, reason.join(' '))); } await Promise.allSettled(promises); } }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const createTwitchClient = () => 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); }; if (!Config.IgnorePNSL) { 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 UserBanError) { return console.warn(`Failed to ban user ${error.username} from ${error.channelName}`); } 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, '').trimEnd(); 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 === 'quit') { process.exit(0); } 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}!`); } } if (!Config.IgnorePNSL) { const data = await fetchList(listID); await runList(data.listData, listID, channel || channelName); } else { const data = await fetchUrlList(listID); await runList(data, listID, channel || channelName); } say(Config.Username, `List ${listID} executed successfully on channel ${channel || channelName}`); } catch (e) { console.error(e); return say(Config.Username, `Error Occurred! ${e.message}`); } } }); setInterval(() => { for (const client of clients) { client.ping(); } }, pingInterval); })(); process .on('unhandledRejection', (err) => { if (err.name === 'SayError') { return; } console.error(`${chalk.red('[UnhandledRejection]')} || [${err.name}] ${err} - ${err.stack}`); }) .on('uncaughtException', (err) => { console.error(`${chalk.red('[UncaughtException]')} || ${err.message}`); process.exit(0); });