Connexion OAuth depuis site + Comptage rewards SQL

This commit is contained in:
Zachary Guénot
2023-05-09 00:32:43 +02:00
parent be73cd765d
commit 172f811e91
19 changed files with 712 additions and 123 deletions

View File

@@ -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) })
}

View 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
View 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) })
}

View 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
View 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
View 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
View 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
View 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
View 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()
}

View File

@@ -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) })
}

View 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
View 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)
}