// PACKAGES const WebSocketClient = require('websocket').client const express = require('express') require('dotenv').config() require('require-all')(__dirname + '/utils/') // VARIABLES let client_id = process.env.TWITCH_APP_ID let client_secret = process.env.TWITCH_APP_SECRET let user_name = process.env.TWITCH_USER_NAME let channel_name = process.env.TWITCH_CHANNEL_NAME let channel_reward_name = process.env.TWITCH_CHANNEL_REWARD_NAME let bot_prefix = process.env.TWITCH_BOT_PREFIX let bot_coubeh = process.env.TWITCH_BOT_COUBEH let bot_mention = process.env.TWITCH_BOT_MENTION const user_scope = ['chat:read', 'chat:edit', 'channel:moderate'] const channel_scope = ['channel:manage:redemptions'] const redirect_uri = 'https://angels-dev.fr/twitch/oauth/login/' const chatBeginMsg = `PRIVMSG #${channel_name} :` const chatReplyMsg = (id) => `@reply-parent-msg-id=${id} ${chatBeginMsg}` // EXPRESS const port = 3000 const app = express() app.use(express.json()) app.use(express.static('public')) // Twitch OAuth app.get('/twitch/oauth/:type', async (req, res) => { console.log(`${new Date().toLocaleString()} - ${req.method} ${req._parsedUrl.pathname} from ${req.socket.remoteAddress}`) let type = req.params.type let url = await oauthGen(client_id, redirect_uri + type, type === 'user' ? user_scope : type === 'channel' ? channel_scope : []) return res.redirect(url) }) app.get('/twitch/oauth/login/:type', async (req, res) => { console.log(`${new Date().toLocaleString()} - ${req.method} ${req._parsedUrl.pathname} from ${req.socket.remoteAddress}`) let type = req.params.type if (type === 'user') { let [user_access_token, user_refresh_token] = await getUserAccessToken(client_id, client_secret, req.query.code, redirect_uri + type) writeEnv('TWITCH_USER_ACCESS_TOKEN', user_access_token) writeEnv('TWITCH_USER_REFRESH_TOKEN', user_refresh_token) user_name = await getUserInfo(client_id, user_access_token, 'login') writeEnv('TWITCH_USER_NAME', user_name) clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') } else if (type === 'channel') { let [channel_access_token, channel_refresh_token] = await getUserAccessToken(client_id, client_secret, req.query.code, redirect_uri + type) writeEnv('TWITCH_CHANNEL_ACCESS_TOKEN', channel_access_token) writeEnv('TWITCH_CHANNEL_REFRESH_TOKEN', channel_refresh_token) channel_name = await getUserInfo(client_id, channel_access_token, 'login') writeEnv('TWITCH_CHANNEL_NAME', channel_name) clientEventSub.connect('wss://eventsub.wss.twitch.tv/ws') } return res.send('Login successful !') }) // Twitch Panel app.get('/twitch/panel/:file', async (req, res) => { console.log(`${new Date().toLocaleString()} - ${req.method} ${req._parsedUrl.pathname} from ${req.socket.remoteAddress}`) let file = req.params.file if (file === 'data') { let panel_data = await getRewardData() //let response = { scoreboard: panel_data, viewer: panel_data.find(entry => entry.viewer_id === panel_viewer_id) } let response = { scoreboard: panel_data } return res.json(response) } else return res.sendFile(__dirname + '/public/panel/' + file) }) app.listen(port, () => { console.log(`[SYS] Express listening on port ${port} !`) }) // CHATBOT const clientChatBot = new WebSocketClient() let connectionChatBot clientChatBot.on('connect', async connection => { connectionChatBot = connection console.log('[SYS] Twitch ChatBot WebSocket Connected !') // Check if the user access token is still valid let [user_access_token, user_name] = await checkUser(process.env.TWITCH_USER_ACCESS_TOKEN) if (user_access_token === 'no_refresh') return console.log("Can't refresh user access token: ", user_name) // Authenticate to Twitch IRC and join channel connection.sendUTF('CAP REQ :twitch.tv/commands twitch.tv/membership twitch.tv/tags') connection.sendUTF(`PASS oauth:${user_access_token}`) connection.sendUTF(`NICK ${user_name}`) connection.sendUTF(`JOIN #${channel_name}`) //connection.sendUTF(`${chatBeginMsg} Salut tout le monde !`) connection.on('message', async message => { if (message.type === 'utf8') { try { let data = parseMessage(message.utf8Data) //console.log(data) // Handle incoming messages if (data.command.command === 'PRIVMSG') { let message = data.parameters.split('\r\n')[0] console.log(`${data.source.nick}: ${message}`) // Handle commands if (message.slice(0, 2) === bot_prefix) { let command = message.split(bot_prefix)[1].split(' ')[0] let args = message.split(' ').slice(1) if (command === 'ping') connection.sendUTF(`${chatReplyMsg(data.tags.id)} Pong !`) else if (command === 'coubeh') { // Restrict to moderators if (data.tags.badges.moderator !== '1') return connection.sendUTF(`${chatReplyMsg(data.tags.id)} Oh lache ça, t'es fou toi !`) if (bot_coubeh === 'true') { bot_coubeh = 'false' connection.sendUTF(`${chatReplyMsg(data.tags.id)} Ok j'arrête de flop les viewers...`) } else { bot_coubeh = 'true' connection.sendUTF(`${chatReplyMsg(data.tags.id)} Aller c'est parti pour faire flop du monde :)))`) } writeEnv('BOT_COUBEH', bot_coubeh) } else if (command === 'mention') { // Restrict to moderators if (data.tags.badges.moderator !== '1') return connection.sendUTF(`${chatReplyMsg(data.tags.id)} Oh lache ça, t'es fou toi !`) if (bot_mention === 'true') { bot_mention = 'false' connection.sendUTF(`${chatReplyMsg(data.tags.id)} Ah tu veux plus que je te répondes ? Ok dac`) } else { bot_mention = 'true' connection.sendUTF(`${chatReplyMsg(data.tags.id)} Maintenant vous allez arrêter de me parler :(((`) } writeEnv('BOT_MENTION', bot_mention) } else if (command === 'arrow') { if ((args.length < 2 || args.length > 4) || !args[1].split('@')[1] || (args.length === 2 && args[0] !== 'get') || (args.length >= 3 && ( (args[0] !== 'add' && args[0] !== 'remove' && args[0] !== 'set') || !parseInt(args[2]) )) || (args.length === 4 && (args[3] !== 'current' && args[3] !== 'permanent')) ) return connection.sendUTF(`${chatReplyMsg(data.tags.id)} Syntaxe: J/arrow <@viewer> | <@viewer> [current|permanent]`) let action = args[0] if (action !== 'get' && data.tags.badges.moderator !== '1') return connection.sendUTF(`${chatReplyMsg(data.tags.id)} Oh lache ça, t'es fou toi !`) let viewer_login = args[1].split('@')[1].toLowerCase() let viewer_id = await getUserID(client_id, user_access_token, viewer_login) let quantity = parseInt(args[2]) if (quantity < 0) return connection.sendUTF(`${chatReplyMsg(data.tags.id)} Tu veux spawn un trou noir dans le stand ou quoi ?!`) let type = args[3] let result = await modifyReward(viewer_id, viewer_login, action, quantity, type) if (result.status === 'get_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${args[1]} a ${result.current_count} flèche(s) dans son carquois et ${result.count} flèche(s) permanente(s) !`) else if (result.status === 'insert_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${args[1]} a été ajouté au stand avec ${quantity} flèche(s) !`) else if (result.status === 'update_add_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${quantity} flèche(s) ajoutée(s) à ${args[1]} !`) else if (result.status === 'update_remove_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${quantity} flèche(s) retirée(s) à ${args[1]} !`) else if (result.status === 'update_set_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${quantity} flèche(s) mises à jour à ${args[1]} !`) else if (result.status === 'no_get_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${args[1]} ne dispose d'aucune flèche, il n'est même pas dans le stand !`) else if (result.status === 'no_remove_entry') connection.sendUTF(`${chatReplyMsg(data.tags.id)} Impossble de retirer des flèches à ${args[1]}, il n'en a aucune !`) else if (result.status === 'not_enough_count') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${args[1]} n'a pas assez de flèches permanentes pour lui en enlever ${quantity} !`) else if (result.status === 'not_enough_current_count') connection.sendUTF(`${chatReplyMsg(data.tags.id)} ${args[1]} n'a pas assez de flèches dans son carquois pour lui en enlever ${quantity} !`) } else if (command == 'leaderboard') { let panel_data = await getRewardData() let leaderboard = '' for (let i = 0; i < 10; i++) { if (panel_data[i]) { leaderboard += `${i + 1}. ${panel_data[i].viewer_name} (${panel_data[i].count} flèches)` if (i < 9) leaderboard += '\n' } } connection.sendUTF(`${chatBeginMsg} ${leaderboard}`) } else connection.sendUTF(`${chatReplyMsg(data.tags.id)} Commande inconnue !`) } else if (message.includes('@Bot_Laytho') && bot_mention === 'true') { connection.sendUTF(`${chatReplyMsg(data.tags.id)} Kestuveu @${data.tags['display-name']} ?`) //connection.sendUTF(`${chatBeginMsg} /timeout ${data.tags['display-name']} 60 T'as pas à me parler comme ça !`) } else if (message.toLowerCase().includes('quoi') && bot_coubeh === 'true') { connection.sendUTF(`${chatReplyMsg(data.tags.id)} @${data.tags['display-name']} Coubeh !`) //connection.sendUTF(`${chatBeginMsg} /timeout ${data.tags['display-name']} 60 T'as pas à me parler comme ça !`) } } else if (data.command.command === 'NOTICE' && data.parameters.includes('Login authentication failed')) { console.log('[SYS] Erreur de connexion ChatBot, veuillez vous reconnecter !\nhttps://angels-dev.fr/twitch/oauth/user') } } catch (error) { } } }) .on('error', error => { console.error(error) }) .on('close', () => { console.log('[SYS] Twitch ChatBot Connection Closed !') clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') }) }).on('connectFailed', error => { console.error(error) }) clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') // EVENTSUB const clientEventSub = new WebSocketClient() let connectionEventSub clientEventSub.on('connect', async connection => { connectionEventSub = connection console.log('[SYS] Twitch EventSub WebSocket Connected !') connection.on('message', async message => { if (message.type === 'utf8') { try { let data = JSON.parse(message.utf8Data) // Check when Twitch asks to login if (data.metadata.message_type === 'session_welcome') { // Check if the channel access token is still valid before connecting let [channel_access_token, channel_name] = await checkChannel(process.env.TWITCH_CHANNEL_ACCESS_TOKEN) if (channel_access_token === 'no_refresh') return console.log("[SYS] Can't refresh channel access token: ", channel_name) // Get broadcaster user id and reward id let broadcaster_user_id = await getUserInfo(client_id, channel_access_token, 'id') writeEnv('TWITCH_CHANNEL_BROADCASTER_ID', broadcaster_user_id) let reward_id = await getRewardID(client_id, channel_access_token, broadcaster_user_id, channel_reward_name) writeEnv('TWITCH_CHANNEL_REWARD_ID', reward_id) let topics = { 'channel.channel_points_custom_reward_redemption.add': { version: '1', condition: { broadcaster_user_id, reward_id } }, 'stream.online': { version: '1', condition: { broadcaster_user_id } } } // Subscribe to all events required for (let type in topics) { console.log(`[SYS] Creating ${type}...`) let { version, condition } = topics[type] let status = await subscribeToEvents(client_id, channel_access_token, data.payload.session.id, type, version, condition) if (!status) return console.error(`[SYS] Failed to create ${type}`) else if (status.error) return console.log('[SYS] Erreur de connexion EventSub, veuillez vous reconnecter !') else console.log(`[SYS] Successfully created ${type}`) } } // Handle notification messages for reward redemption else if (data.metadata.message_type === 'notification') { let { subscription, event } = data.payload if (subscription.type === 'channel.channel_points_custom_reward_redemption.add') { console.log(subscription) console.log(event) let viewer_id = event.user_id let viewer_name = event.user_name console.log(`[SYS] Viewer ${viewer_name} claimed reward ${event.reward.title} !`) let channel_access_token = process.env.TWITCH_CHANNEL_ACCESS_TOKEN let broadcaster_user_id = await getUserInfo(client_id, channel_access_token, 'id') let reward_id = await getRewardID(client_id, channel_access_token, broadcaster_user_id, channel_reward_name) let result = await rewardRedemption(viewer_id, viewer_name, client_id, channel_access_token, event.id, broadcaster_user_id, reward_id) if (result.status === 'insert_entry') { return connectionChatBot.sendUTF(`${chatBeginMsg} @${viewer_name} a récupéré sa première ${event.reward.title}, GG !`) } else if (result.status === 'update_entry') { return connectionChatBot.sendUTF(`${chatBeginMsg} @${viewer_name} a récupéré sa ${event.reward.title}, t'en as ${result.current_count} dans ton carquois et récupéré ${result.count} depuis le début !`) } } else if (subscription.type === 'stream.online') { console.log(`[SYS] Stream from ${event.broadcaster_user_name} is now online, connecting to chat...`) clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') } } // Don't log ping/pong messages else if (data.metadata.message_type === 'session_keepalive') return // Log unknown messages else console.log(data) } catch (error) { console.error(error) } } }) .on('error', error => { console.error(error) }) .on('close', () => { console.log('[SYS] Twitch EventSub Connection Closed !') clientEventSub.connect('wss://eventsub.wss.twitch.tv/ws') }) }).on('connectFailed', error => { console.error(error) }) clientEventSub.connect('wss://eventsub.wss.twitch.tv/ws')