diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2cfa5d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +**/.dockerignore +**/.git +**/.gitignore +**/.vscode +**/docker-compose* +**/Dockerfile* +**/node_modules +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index e762de3..ffe384f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.env +/.eslintrc.json /node_modules /package-lock.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e7708c7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch", + "program": "${workspaceFolder}/app.js", + "skipFiles": [ + "/**" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Nodemon", + "program": "${workspaceFolder}/app.js", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "nodemon", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "restart": true + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f039dda --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,39 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "docker-build", + "label": "docker-build", + "platform": "node", + "dockerBuild": { + "dockerfile": "${workspaceFolder}/Dockerfile", + "context": "${workspaceFolder}", + "pull": true + } + }, + { + "type": "docker-run", + "label": "docker-run: release", + "dependsOn": [ + "docker-build" + ], + "platform": "node" + }, + { + "type": "docker-run", + "label": "docker-run: debug", + "dependsOn": [ + "docker-build" + ], + "dockerRun": { + "env": { + "DEBUG": "*", + "NODE_ENV": "development" + } + }, + "node": { + "enableDebugging": true + } + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..1401abd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:latest +ENV NODE_ENV=production +WORKDIR /usr/src/app +COPY ["package.json", "package-lock.json*", "./"] +RUN npm install --production --silent && mv node_modules ../ +COPY . . +RUN chown -R node /usr/src/app +USER node +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/app.js b/app.js index d08bb92..622c809 100644 --- a/app.js +++ b/app.js @@ -1,57 +1,186 @@ // PACKAGES -const tmi = require('tmi.js') const WebSocketClient = require('websocket').client -const axios = require('axios') +const express = require('express') require('dotenv').config() + // UTILS -const getAccessToken = require('./utils/getAccessToken') -const subscribeToEvent = require('./utils/subscribeToEvent') +const getReward = require('./utils/getReward') +const getUserAccessToken = require('./utils/getUserAccessToken') +const getUserID = require('./utils/getUserID') +const getUserName = require('./utils/getUserName') +const oauthGen = require('./utils/oauthGen') +const parseMessage = require('./utils/parseMessage') +const rewardRedemption = require('./utils/rewardRedemption') +const subscribeToEvents = require('./utils/subscribeToEvents') +const writeEnv = require('./utils/writeEnv') + + +// VARIABLES +let client_id = process.env.TWITCH_APP_ID +let client_secret = process.env.TWITCH_APP_SECRET + +let user_access_token = process.env.TWITCH_USER_ACCESS_TOKEN +let user_name = process.env.TWITCH_USER_USERNAME + +let channel_access_token = process.env.TWITCH_CHANNEL_ACCESS_TOKEN +let channel_name = process.env.TWITCH_CHANNEL_USERNAME + +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}` + + +// EXPRESS +const port = 3000 +const app = express() +app.use(express.json()) +app.use(express.static('panel')) + +app.get('/twitch/oauth/:type', async (req, res) => { + 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(req.query) + let type = req.params.type + + if (type === 'user') { + user_access_token = await getUserAccessToken(req.query.code, client_id, client_secret, redirect_uri + type) + writeEnv('TWITCH_USER_ACCESS_TOKEN', user_access_token) + + user_name = await getUserName(client_id, user_access_token) + writeEnv('TWITCH_USER_USERNAME', user_name) + + clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') + } + else if (type === 'channel') { + channel_access_token = await getUserAccessToken(req.query.code, client_id, client_secret, redirect_uri + type) + writeEnv('TWITCH_CHANNEL_ACCESS_TOKEN', channel_access_token) + + channel_name = await getUserName(client_id, channel_access_token) + writeEnv('TWITCH_CHANNEL_USERNAME', channel_name) + + clientEventSub.connect('wss://eventsub.wss.twitch.tv/ws') + } + return res.send('Login successful !') +}) + +app.listen(port, () => { console.log(`Listening at ${redirect_uri}`) }) + // CHATBOT -const chatBotC = new tmi.Client({ - options: { debug: true }, - identity: { - username: process.env.TWITCH_USERNAME, - password: `oauth:${process.env.TWITCH_TOKEN}` - }, - channels: [ process.env.TWITCH_CHANNEL ] -}) +const clientChatBot = new WebSocketClient() +let connectionChatBot -chatBotC.on('message', async (channel, tags, message, self) => { - if (self) return - if (message.toLowerCase() === '!hello') { - console.log('Command "hello" was triggered in channel: ' + channel) - chatBotC.say(channel, `@${tags.username}, heya!`) - } -}) +clientChatBot.on('connect', async connection => { + console.log('Twitch ChatBot WebSocket Connected !') + connectionChatBot = connection + + // 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(`PRIVMSG #${channel_name} :Salut tout le monde !`) + + connection.on('message', async message => { + if (message.type === 'utf8') { + try { + let data = parseMessage(message.utf8Data) + + // Handle incoming messages + if (data.command.command === 'PRIVMSG') { + let message = data.parameters.split('\r\n')[0] + console.log(`${data.source.nick}: ${message}`) + + if (message.includes('@Bot_Laytho')) { + connection.sendUTF(`@reply-parent-msg-id=${data.tags.id} ${chatBeginMsg} :Kestuveu @${data.tags['display-name']} ?`) + connection.sendUTF(`${chatBeginMsg} :/timeout ${data.tags['display-name']} 60 T'as pas à me parler comme ça !`) + } + else if (message === '!ping') { + connection.sendUTF(`@reply-parent-msg-id=${data.tags.id} ${chatBeginMsg} :Pong !`) + } + } else if (data.command.command === 'NOTICE') { + if (data.parameters.includes('Login authentication failed')) { + console.log('Erreur de connexion ChatBot, veuillez vous reconnecter !\nhttps://angels-dev.fr/twitch/oauth/user') + } + } + } catch (error) { } // catch (error) { console.error(error) } + } }) + .on('error', error => { console.error(error) }) + .on('close', () => { console.log('Twitch ChatBot Connection Closed !') }) +}).on('connectFailed', error => { console.error(error) }) + +clientChatBot.connect('wss://irc-ws.chat.twitch.tv:443') -//chatBotC.connect() // EVENTSUB -const eventSubC = new WebSocketClient() - -eventSubC.on('connect', async connection => { - console.log('WebSocket eventSub Connected') - connection.sendUTF('CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands') - connection.sendUTF(`PASS oauth:${process.env.TWITCH_APP_SECRET}`) - connection.sendUTF('NICK bot_Laytho') - connection.sendUTF('JOIN #liveAngels') +const clientEventSub = new WebSocketClient().on('connect', async connection => { + console.log('Twitch EventSub WebSocket Connected !') connection.on('message', async message => { if (message.type === 'utf8') { - console.log("Received: '" + message.utf8Data + "'") try { let data = JSON.parse(message.utf8Data) - console.log(data) + + // Check when Twitch asks to login if (data.metadata.message_type === 'session_welcome') { - let access_token = await getAccessToken(process.env.TWITCH_APP_ID, process.env.TWITCH_APP_SECRET) - await subscribeToEvent(access_token, data.payload.session.id, process.env.TWITCH_APP_ID) - } - } catch (e) { console.log(e) } - } }) - .on('error', error => { console.log("Connection Error: " + error.toString()) }) - .on('close', () => { console.log('echo-protocol Connection Closed') }) -}).on('connectFailed', error => { console.log('Connect Error: ' + error.toString()) }) -eventSubC.connect('wss://eventsub.wss.twitch.tv/ws') + // Get broadcaster user id and reward id + let broadcaster_user_id = await getUserID(client_id, channel_access_token, channel_name) + let reward_id = await getReward(client_id, channel_access_token, broadcaster_user_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(`Creating ${type}...`) + let { version, condition } = topics[type] + + let status = await subscribeToEvents(channel_access_token, data.payload.session.id, client_id, type, version, condition) + if (!status) return console.error(`Failed to create ${type}`) + + else if (status.error) { + console.error(status) + console.log('Erreur de connexion EventSub, veuillez vous reconnecter !\nhttps://angels-dev.fr/twitch/oauth/channel') + return connection.sendUTF(`${chatBeginMsg} :@${channel_name} Erreur de connexion EventSub, veuillez vous reconnecter !\nhttps://angels-dev.fr/twitch/oauth/channel`) + } else console.log(`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' && event.reward.id === reward_id) { + if (subscription.type === 'channel.channel_points_custom_reward_redemption.add') { + let { user_id, user_name } = event + console.log(`User ${user_name} claimed reward ${event.reward.title} !`) + rewardRedemption(user_id, user_name) + } + else if (subscription.type === 'stream.online' && event.broadcaster_user_login === channel_name) { + console.log(`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('Twitch EventSub Connection Closed !') }) +}).on('connectFailed', error => { console.error(error) }) + +clientEventSub.connect('wss://eventsub.wss.twitch.tv/ws') \ No newline at end of file diff --git a/package.json b/package.json index 437f709..b73e497 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,29 @@ -{ - "dependencies": { - "axios": "^1.4.0", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "open": "^9.1.0", - "tmi.js": "^1.8.5", - "websocket": "^1.0.34" - } -} +{ + "name": "bot_laytho", + "version": "1.0.0", + "description": "bot_Laytho", + "main": "app.js", + "scripts": { + "format": "prettier --write .", + "start": "node app.js", + "dev": "nodemon -e js" + }, + "author": { + "name": "Angels / Jojo" + }, + "devDependencies": { + "eslint": "^8.40.0", + "nodemon": "^2.0.22", + "prettier": "^2.8.8" + }, + "eslintConfig": {}, + "dependencies": { + "axios": "^1.4.0", + "dotenv": "^16.0.3", + "envfile": "^6.18.0", + "express": "^4.18.2", + "mysql": "^2.18.1", + "tmi.js": "^1.8.5", + "websocket": "^1.0.34" + } +} diff --git a/utils/getAccessToken.js b/utils/getAccessToken.js deleted file mode 100644 index bb90771..0000000 --- a/utils/getAccessToken.js +++ /dev/null @@ -1,50 +0,0 @@ -const axios = require('axios') -const express = require('express') -//const open = require('open') -const open = (...args) => import('open').then(({default: open}) => open(...args)) - -module.exports = async (client_id, client_secret) => { - /* - return await axios.post('https://id.twitch.tv/oauth2/token', { - client_id, - client_secret, - grant_type: 'client_credentials', - scope: 'channel:manage:redemptions' - }).then(response => { - console.log(response.data) - if (response.data.token_type === 'bearer') return response.data.access_token - }).catch(error => { console.log(error) }) - */ - - // Listen on port 3000 for twitch to send us the access token - const app = express() - const port = 3000 - - app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) }) - - // Open the browser to the twitch login page - await open(`https://id.twitch.tv/oauth2/authorize?client_id=${client_id}&redirect_uri=http://localhost:3000&response_type=code&scope=channel:manage:redemptions`) - - // Wait for the access token to be sent to us - let code = await new Promise((resolve, reject) => { - app.get('/', (req, res) => { - console.log(req.query) - res.send('Hello World!') - resolve(req.query.code) - }) - }) - - // Use the access token to get the oauth token - return await axios.post('https://id.twitch.tv/oauth2/token', { - client_id, - client_secret, - code, - grant_type: 'authorization_code', - redirect_uri: 'http://localhost:3000' - }).then(response => { - console.log(response.data) - if (response.data.token_type === 'bearer') return response.data.access_token - }).catch(error => { console.log(error) }) - - -} \ No newline at end of file diff --git a/utils/getAppAccessToken.js b/utils/getAppAccessToken.js new file mode 100644 index 0000000..608c3ce --- /dev/null +++ b/utils/getAppAccessToken.js @@ -0,0 +1,12 @@ +const axios = require('axios') + +module.exports = async function (client_id, client_secret) { + return await axios.post('https://id.twitch.tv/oauth2/token', { + client_id, + client_secret, + grant_type: 'client_credentials' + }).then(response => { + //console.log(response.data) + if (response.data.token_type === 'bearer') return response.data.access_token + }).catch(error => { console.log(error.response.data) }) +} \ No newline at end of file diff --git a/utils/getReward.js b/utils/getReward.js new file mode 100644 index 0000000..445d5ac --- /dev/null +++ b/utils/getReward.js @@ -0,0 +1,13 @@ +const axios = require('axios') + +module.exports = async function (client_id, access_token, broadcaster_id) { + return await axios.get(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${broadcaster_id}`, { + headers: { + 'Authorization': `Bearer ${access_token}`, + 'Client-Id': client_id + } + }).then(response => { + console.log(response.data) + return response.data.data[0].id + }).catch(error => { console.log(error.response.data) }) +} \ No newline at end of file diff --git a/utils/getUserAccessToken.js b/utils/getUserAccessToken.js new file mode 100644 index 0000000..5d31394 --- /dev/null +++ b/utils/getUserAccessToken.js @@ -0,0 +1,14 @@ +const axios = require('axios') + +module.exports = async function (code, client_id, client_secret, redirect_uri) { + return await axios.post('https://id.twitch.tv/oauth2/token', { + code, + client_id, + client_secret, + redirect_uri, + grant_type: 'authorization_code' + }).then(response => { + console.log(response.data) + if (response.data.token_type === 'bearer') return response.data.access_token + }).catch(error => { console.log(error.response.data) }) +} \ No newline at end of file diff --git a/utils/getUserID.js b/utils/getUserID.js new file mode 100644 index 0000000..d855c96 --- /dev/null +++ b/utils/getUserID.js @@ -0,0 +1,13 @@ +const axios = require('axios') + +module.exports = async function (client_id, access_token, login) { + return await axios.get(`https://api.twitch.tv/helix/users?login=${login}`, { + headers: { + 'Authorization': `Bearer ${access_token}`, + 'Client-Id': client_id + } + }).then(response => { + //console.log(response.data) + return response.data.data[0].id + }).catch(error => { console.log(error.response.data) }) +} \ No newline at end of file diff --git a/utils/getUserName.js b/utils/getUserName.js new file mode 100644 index 0000000..f30ea33 --- /dev/null +++ b/utils/getUserName.js @@ -0,0 +1,13 @@ +const axios = require('axios') + +module.exports = async function (client_id, access_token) { + return await axios.get(`https://api.twitch.tv/helix/users`, { + headers: { + 'Authorization': `Bearer ${access_token}`, + 'Client-Id': client_id + } + }).then(response => { + console.log(response.data) + return response.data.data[0].login + }).catch(error => { console.log(error.response.data) }) +} \ No newline at end of file diff --git a/utils/oauthGen.js b/utils/oauthGen.js new file mode 100644 index 0000000..b5dba34 --- /dev/null +++ b/utils/oauthGen.js @@ -0,0 +1,10 @@ +module.exports = async function (client_id, redirect_uri, scope) { + console.log(scope) + let queries = { + response_type: 'code', + client_id, + redirect_uri, + scope: scope.join('+') + } + return `https://id.twitch.tv/oauth2/authorize?${Object.keys(queries).map(key=>`${key}=${queries[key]}`).join('&')}` +} \ No newline at end of file diff --git a/utils/parseMessage.js b/utils/parseMessage.js new file mode 100644 index 0000000..d276ec9 --- /dev/null +++ b/utils/parseMessage.js @@ -0,0 +1,286 @@ +// Parses an IRC message and returns a JSON object with the message's +// component parts (tags, source (nick and host), command, parameters). +// Expects the caller to pass a single message. (Remember, the Twitch +// IRC server may send one or more IRC messages in a single message.) + +module.exports = function parseMessage(message) { + + let parsedMessage = { // Contains the component parts. + tags: null, + source: null, + command: null, + parameters: null + }; + + // The start index. Increments as we parse the IRC message. + + let idx = 0; + + // The raw components of the IRC message. + + let rawTagsComponent = null; + let rawSourceComponent = null; + let rawCommandComponent = null; + let rawParametersComponent = null; + + // If the message includes tags, get the tags component of the IRC message. + + if (message[idx] === '@') { // The message includes tags. + let endIdx = message.indexOf(' '); + rawTagsComponent = message.slice(1, endIdx); + idx = endIdx + 1; // Should now point to source colon (:). + } + + // Get the source component (nick and host) of the IRC message. + // The idx should point to the source part; otherwise, it's a PING command. + + if (message[idx] === ':') { + idx += 1; + let endIdx = message.indexOf(' ', idx); + rawSourceComponent = message.slice(idx, endIdx); + idx = endIdx + 1; // Should point to the command part of the message. + } + + // Get the command component of the IRC message. + + let endIdx = message.indexOf(':', idx); // Looking for the parameters part of the message. + if (-1 == endIdx) { // But not all messages include the parameters part. + endIdx = message.length; + } + + rawCommandComponent = message.slice(idx, endIdx).trim(); + + // Get the parameters component of the IRC message. + + if (endIdx != message.length) { // Check if the IRC message contains a parameters component. + idx = endIdx + 1; // Should point to the parameters part of the message. + rawParametersComponent = message.slice(idx); + } + + // Parse the command component of the IRC message. + + parsedMessage.command = parseCommand(rawCommandComponent); + + // Only parse the rest of the components if it's a command + // we care about; we ignore some messages. + + if (null == parsedMessage.command) { // Is null if it's a message we don't care about. + return null; + } + else { + if (null != rawTagsComponent) { // The IRC message contains tags. + parsedMessage.tags = parseTags(rawTagsComponent); + } + + parsedMessage.source = parseSource(rawSourceComponent); + + parsedMessage.parameters = rawParametersComponent; + if (rawParametersComponent && rawParametersComponent[0] === '!') { + // The user entered a bot command in the chat window. + parsedMessage.command = parseParameters(rawParametersComponent, parsedMessage.command); + } + } + + return parsedMessage; +} + +// Parses the tags component of the IRC message. + +function parseTags(tags) { + // badge-info=;badges=broadcaster/1;color=#0000FF;... + + const tagsToIgnore = { // List of tags to ignore. + 'client-nonce': null, + 'flags': null + }; + + let dictParsedTags = {}; // Holds the parsed list of tags. + // The key is the tag's name (e.g., color). + let parsedTags = tags.split(';'); + + parsedTags.forEach(tag => { + let parsedTag = tag.split('='); // Tags are key/value pairs. + let tagValue = (parsedTag[1] === '') ? null : parsedTag[1]; + + switch (parsedTag[0]) { // Switch on tag name + case 'badges': + case 'badge-info': + // badges=staff/1,broadcaster/1,turbo/1; + + if (tagValue) { + let dict = {}; // Holds the list of badge objects. + // The key is the badge's name (e.g., subscriber). + let badges = tagValue.split(','); + badges.forEach(pair => { + let badgeParts = pair.split('/'); + dict[badgeParts[0]] = badgeParts[1]; + }) + dictParsedTags[parsedTag[0]] = dict; + } + else { + dictParsedTags[parsedTag[0]] = null; + } + break; + case 'emotes': + // emotes=25:0-4,12-16/1902:6-10 + + if (tagValue) { + let dictEmotes = {}; // Holds a list of emote objects. + // The key is the emote's ID. + let emotes = tagValue.split('/'); + emotes.forEach(emote => { + let emoteParts = emote.split(':'); + + let textPositions = []; // The list of position objects that identify + // the location of the emote in the chat message. + let positions = emoteParts[1].split(','); + positions.forEach(position => { + let positionParts = position.split('-'); + textPositions.push({ + startPosition: positionParts[0], + endPosition: positionParts[1] + }) + }); + + dictEmotes[emoteParts[0]] = textPositions; + }) + + dictParsedTags[parsedTag[0]] = dictEmotes; + } + else { + dictParsedTags[parsedTag[0]] = null; + } + + break; + case 'emote-sets': + // emote-sets=0,33,50,237 + + let emoteSetIds = tagValue.split(','); // Array of emote set IDs. + dictParsedTags[parsedTag[0]] = emoteSetIds; + break; + default: + // If the tag is in the list of tags to ignore, ignore + // it; otherwise, add it. + + if (tagsToIgnore.hasOwnProperty(parsedTag[0])) { + ; + } + else { + dictParsedTags[parsedTag[0]] = tagValue; + } + } + }); + + return dictParsedTags; +} + +// Parses the command component of the IRC message. + +function parseCommand(rawCommandComponent) { + let parsedCommand = null; + commandParts = rawCommandComponent.split(' '); + + switch (commandParts[0]) { + case 'JOIN': + case 'PART': + case 'NOTICE': + case 'CLEARCHAT': + case 'HOSTTARGET': + case 'PRIVMSG': + parsedCommand = { + command: commandParts[0], + channel: commandParts[1] + } + break; + case 'PING': + parsedCommand = { + command: commandParts[0] + } + break; + case 'CAP': + parsedCommand = { + command: commandParts[0], + isCapRequestEnabled: (commandParts[2] === 'ACK') ? true : false, + // The parameters part of the messages contains the + // enabled capabilities. + } + break; + case 'GLOBALUSERSTATE': // Included only if you request the /commands capability. + // But it has no meaning without also including the /tags capability. + parsedCommand = { + command: commandParts[0] + } + break; + case 'USERSTATE': // Included only if you request the /commands capability. + case 'ROOMSTATE': // But it has no meaning without also including the /tags capabilities. + parsedCommand = { + command: commandParts[0], + channel: commandParts[1] + } + break; + case 'RECONNECT': + console.log('The Twitch IRC server is about to terminate the connection for maintenance.') + parsedCommand = { + command: commandParts[0] + } + break; + case '421': + console.log(`Unsupported IRC command: ${commandParts[2]}`) + return null; + case '001': // Logged in (successfully authenticated). + parsedCommand = { + command: commandParts[0], + channel: commandParts[1] + } + break; + case '002': // Ignoring all other numeric messages. + case '003': + case '004': + case '353': // Tells you who else is in the chat room you're joining. + case '366': + case '372': + case '375': + case '376': + console.log(`numeric message: ${commandParts[0]}`) + return null; + default: + console.log(`\nUnexpected command: ${commandParts[0]}\n`); + return null; + } + + return parsedCommand; +} + +// Parses the source (nick and host) components of the IRC message. + +function parseSource(rawSourceComponent) { + if (null == rawSourceComponent) { // Not all messages contain a source + return null; + } + else { + let sourceParts = rawSourceComponent.split('!'); + return { + nick: (sourceParts.length == 2) ? sourceParts[0] : null, + host: (sourceParts.length == 2) ? sourceParts[1] : sourceParts[0] + } + } +} + +// Parsing the IRC parameters component if it contains a command (e.g., !dice). + +function parseParameters(rawParametersComponent, command) { + let idx = 0 + let commandParts = rawParametersComponent.slice(idx + 1).trim(); + let paramsIdx = commandParts.indexOf(' '); + + if (-1 == paramsIdx) { // no parameters + command.botCommand = commandParts.slice(0); + } + else { + command.botCommand = commandParts.slice(0, paramsIdx); + command.botCommandParams = commandParts.slice(paramsIdx).trim(); + // TODO: remove extra spaces in parameters string + } + + return command; +} \ No newline at end of file diff --git a/utils/rewardRedemption.js b/utils/rewardRedemption.js new file mode 100644 index 0000000..2e6d4a3 --- /dev/null +++ b/utils/rewardRedemption.js @@ -0,0 +1,38 @@ +const mysql = require('mysql') + +module.exports = function rewardRedemption(user_id, user_name) { + // Create a connection to the MySQL database + const connection = mysql.createConnection({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE + }) + + // Connect to the database + connection.connect(error => { + if (error) return console.error(error) + console.log(`Connected to MySql database as id ${connection.threadId} !`) + }) + + // Check if the user already exists in the rewards table + connection.query('SELECT * FROM rewards WHERE user_id = ?', [user_id], (error, results) => { + if (error) return console.error(error) + + if (results.length === 0) { + // User doesn't exist, insert a new row + connection.query('INSERT INTO rewards SET ?', { user_id, user_name, count: 1 }, error => { + if (error) return console.error(error) + }) + } else { + // User exists, update the count + const newRow = { count: results[0].count + 1 } + connection.query('UPDATE rewards SET ? WHERE user_id = ?', [newRow, user_id], error => { + if (error) return console.error(error) + }) + } + }) + + // Terminate the connection to the database + connection.end() +} \ No newline at end of file diff --git a/utils/subscribeToEvent.js b/utils/subscribeToEvent.js deleted file mode 100644 index e0c1fa0..0000000 --- a/utils/subscribeToEvent.js +++ /dev/null @@ -1,24 +0,0 @@ -const axios = require('axios') - -module.exports = async (access_token, session_id, client_id) => { - await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', { - type: 'channel.channel_points_custom_reward_redemption.add', - version: '1', - condition: { - broadcaster_user_id: '1337', - reward_id: 'abcf127c-7326-4483-a52b-b0da0be61c01' - }, - transport: { - method: 'websocket', - session_id - } - }, { - headers: { - 'Authorization': `Bearer ${access_token}`, - 'Client-Id': client_id, - 'Content-Type': 'application/json' - } - }).then(response => { - console.log(response.data) - }).catch(error => { console.log(error) }) -} \ No newline at end of file diff --git a/utils/subscribeToEvents.js b/utils/subscribeToEvents.js new file mode 100644 index 0000000..af73f94 --- /dev/null +++ b/utils/subscribeToEvents.js @@ -0,0 +1,21 @@ +const axios = require('axios') + +module.exports = async function (access_token, session_id, client_id, type, version, condition) { + return await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', { + type, + version, + condition, + transport: { + method: 'websocket', + session_id + } + }, { + headers: { + 'Authorization': `Bearer ${access_token}`, + 'Client-Id': client_id, + 'Content-Type': 'application/json' + } + }).then(response => { + return response.data.data[0].status + }).catch(error => { return error.response.data }) +} \ No newline at end of file diff --git a/utils/writeEnv.js b/utils/writeEnv.js new file mode 100644 index 0000000..f56bdb0 --- /dev/null +++ b/utils/writeEnv.js @@ -0,0 +1,7 @@ +const fs = require('fs') + +module.exports = function (variable, value) { + let parsedFile = fs.readFileSync('./.env', 'utf8') + parsedFile = parsedFile.replace(new RegExp(`${variable}=.*`, 'g'), `${variable}=${value}`) + fs.writeFileSync('./.env', parsedFile) +} \ No newline at end of file