Connexion OAuth depuis site + Comptage rewards SQL
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.vscode
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
README.md
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/.env
|
||||
/.eslintrc.json
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@@ -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": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Nodemon",
|
||||
"program": "${workspaceFolder}/app.js",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"runtimeExecutable": "nodemon",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"restart": true
|
||||
}
|
||||
]
|
||||
}
|
||||
39
.vscode/tasks.json
vendored
Normal file
39
.vscode/tasks.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
Dockerfile
Executable file
10
Dockerfile
Executable file
@@ -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"]
|
||||
207
app.js
207
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')
|
||||
39
package.json
39
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) })
|
||||
|
||||
|
||||
}
|
||||
12
utils/getAppAccessToken.js
Normal file
12
utils/getAppAccessToken.js
Normal file
@@ -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) })
|
||||
}
|
||||
13
utils/getReward.js
Normal file
13
utils/getReward.js
Normal file
@@ -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) })
|
||||
}
|
||||
14
utils/getUserAccessToken.js
Normal file
14
utils/getUserAccessToken.js
Normal file
@@ -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) })
|
||||
}
|
||||
13
utils/getUserID.js
Normal file
13
utils/getUserID.js
Normal file
@@ -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) })
|
||||
}
|
||||
13
utils/getUserName.js
Normal file
13
utils/getUserName.js
Normal file
@@ -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) })
|
||||
}
|
||||
10
utils/oauthGen.js
Normal file
10
utils/oauthGen.js
Normal file
@@ -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('&')}`
|
||||
}
|
||||
286
utils/parseMessage.js
Normal file
286
utils/parseMessage.js
Normal file
@@ -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;
|
||||
}
|
||||
38
utils/rewardRedemption.js
Normal file
38
utils/rewardRedemption.js
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
21
utils/subscribeToEvents.js
Normal file
21
utils/subscribeToEvents.js
Normal file
@@ -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 })
|
||||
}
|
||||
7
utils/writeEnv.js
Normal file
7
utils/writeEnv.js
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user