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 = []; let listenClient = null; 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, sourceChannel) => { const banArr = listData.split('\n'); const targetChannel = Config.UseParallelUniverse === true ? channel : `#${channel}`; 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(targetChannel, user, reason.join(' '))); } await Promise.allSettled(promises); say(sourceChannel, `${Config.UseParallelUniverse === true ? '[PU]' : ''} Chunk ${currentChunk}/${chunkCount} of ${listID} executed successfully on channel ${targetChannel}`); if (Config.SendChunks) say(channel, `Chunk ${currentChunk}/${chunkCount} executed successfully`); 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(targetChannel, 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) => { if (channel.startsWith('#')) { listenClient.say(channel, message); } else { listenClient.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('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())); })); listenClient = createTwitchClient(); listenClient.connect(); listenClient.on('ready', async () => { if (Config.Channels.length !== 0) { await listenClient.joinAll(Config.Channels); } console.log(`${chalk.green('[TWITCH]')} || Command listener connected`); }); console.log(`${chalk.green('[TWITCH]')} || ${clients.filter((i) => i.ready).length} Clients connected`); listenClient.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]; let channel = args[1]; if (!listID) { return say(channelName, 'No list ID provided!'); } if (!channel) { channel = Config.DefaultChannel || channelName; } if (Config.Blacklisted.includes(channel)) { return say(channelName, `I am not allowed to execute lists in channel ${channel}!`); } try { if (channel != channelName) { const mods = await listenClient.getMods(`#${channel}`); if (!mods.includes(Config.Username)) { return say(channelName, `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(channelName, `${Config.UseParallelUniverse === true ? '[PU]' : ''} List ${listID} executed successfully on channel ${channel || channelName}`); if (Config.SendMeme) say(channel, 'FeelsGoodMan'); } catch (e) { console.error(e); return say(channelName, `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); });