From 0c6deae544aeed60bde428d3d94b2abce336a2a2 Mon Sep 17 00:00:00 2001 From: DeSqBlocki Date: Sat, 8 Feb 2025 13:42:18 +0100 Subject: [PATCH] Raw Dogged that custom shoutout event and made HelixRequest easier to handle universally --- .env.sample | 1 - clients.js | 2 +- events/Twitch/Message.js | 49 ++++++-------------- events/Twitch/Raided.js | 6 ++- events/Twitch/Shoutout.js | 65 +++++++++++++++++++++++++++ events/Twitch/Subscribers.js | 14 +++++- handlers/events.js | 4 +- index.js | 86 ++++++++++++++++++++++++++++-------- 8 files changed, 165 insertions(+), 62 deletions(-) create mode 100644 events/Twitch/Shoutout.js diff --git a/.env.sample b/.env.sample index 2a99c12..546feb6 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,2 @@ MONGO_URI= -MONGO_DB= PREFIX= \ No newline at end of file diff --git a/clients.js b/clients.js index 859d2da..6b641a4 100644 --- a/clients.js +++ b/clients.js @@ -25,7 +25,7 @@ const initializeClients = async () => { ], }); - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("client"); const credentialCollection = db.collection('credentials'); const dCreds = await credentialCollection.findOne({ service: 'discord' }); const tCreds = await credentialCollection.findOne({ service: 'twitch' }); diff --git a/events/Twitch/Message.js b/events/Twitch/Message.js index c6ab2c4..c0066df 100644 --- a/events/Twitch/Message.js +++ b/events/Twitch/Message.js @@ -3,41 +3,19 @@ module.exports = { name: 'Twitch/Message', once: false, async execute(channel, userstate, message, self, tClient) { - const { dClient, mClient, makeTwitchRequest } = require('../../index') + const { dClient, mClient, getIDByName, updateChatMode} = require('../../index') const knownBots = new Set(['streamlabs', 'nightbot', 'moobot', 'soundalerts', 'streamelements', 'remasuri_bot', 'commanderroot', 'x__hel_bot__x']) - async function updateChatMode(userID, setTo) { - let state - if (setTo === 'on') { - state = true - } else if (setTo === 'off') { - state = false - } else { - return - } - await Helix.chat.updateSettings(userID, process.env.MOD_ID, { - follower_mode: state, - follower_mode_duration: 0, - subscriber_mode: state, - emote_mode: state - }); - - } - async function getIDByName(user) { - // Function to convert Names to ID - const result = await Helix.users.get(user); - return result.data[0].id - } - async function verifyUser(channel, username) { - // Function checks if user is a target to shout out, default target: VIPs + async function emitShoutoutInfo(channel, username) { + // Function sends information to the Shoutout Handler if they haven't been shouted out before, default target: VIPs let db = mClient.db("shoutouts") let col = db.collection(channel) - - let query = { user: username } - let status = await col.findOne(query) + let status = await col.findOne({ user: username }) + if (!status) { - dClient.emit('TwitchShoutout', "VIP", channel, username) + dClient.emit('Twitch/Shoutout', "VIP", channel, username, tClient) await col.insertOne({ - user: username + user: username, + created_at: Date() }) } } @@ -61,6 +39,7 @@ module.exports = { return tClient.say(channel, positive_Messages[rdm]) } async function sendDonationAd() { + // Function to send static donate ad const links = ["https://bit.ly/heltwittergoals", "https://bit.ly/donatetohel", "http://bit.ly/donothon_goals"] tClient.say(channel, "Hel is currently celebrating her birthday with a donathon!") tClient.say(channel, `You can see goals and incentives here: ${links[0]}`) @@ -85,9 +64,6 @@ module.exports = { const args = message.substring(1).split(" ") const cmd = args[0] switch (cmd) { - // case "test": - // tClient.say(channel, "/me This is a test message") - // break; case "mode": if (userstate.badges.moderator != 1 && userstate.badges.broadcaster != 1) { break } // return stops all further operations, so you should just break out of switch let regex = /^(on|off)$/ @@ -98,7 +74,8 @@ module.exports = { sendDonationAd() break; case "test": - console.log(await makeTwitchRequest('games/top')) + await emitShoutoutInfo(channel, userstate.username) + // console.log(await getIDByName(userstate)) break; case "positivity": sendPositivity() @@ -109,8 +86,8 @@ module.exports = { } // ---------------VIP Handler------------------- - // if (!userstate.badges?.vip) { return } - // verifyUser(channel, userstate.username) + if (!userstate.badges?.vip) { return } + emitShoutoutInfo(channel, userstate.username) // --------------------------------------------- } diff --git a/events/Twitch/Raided.js b/events/Twitch/Raided.js index 0ffe02d..eab2f9e 100644 --- a/events/Twitch/Raided.js +++ b/events/Twitch/Raided.js @@ -2,7 +2,9 @@ module.exports = { name: 'Twitch/Raided', once: false, - execute(stream) { - console.log(`Connected to ${stream.address}`) + execute(channel, username, raiders, tClient) { + const { dClient } = require('../../index') + tClient.say(channel, `${username} entrusted us with ${raiders} people of their community, welcome xhelxBlankie`) + dClient.emit('Twitch/Shoutout', "RAID", channel, username) } } \ No newline at end of file diff --git a/events/Twitch/Shoutout.js b/events/Twitch/Shoutout.js new file mode 100644 index 0000000..19eee50 --- /dev/null +++ b/events/Twitch/Shoutout.js @@ -0,0 +1,65 @@ +// Event triggered by Custom Shoutout emitted in Messages.js +const soBuffer = new Map(); +let isShoutoutInProgress = false; // Track shoutout status to avoid overlapping shoutouts + +module.exports = { + name: 'Twitch/Shoutout', + once: false, + async execute(reason, channel, username, tClient) { + const { getIDByName, getChannelInformation } = require('../../index'); + + // Function to perform shoutout (Nightbot or custom) + async function doShoutouts(channel, user) { + try { + // Use Nightbot command !so + // tClient.say(channel, `!so ${user}`); // Send the shoutout command + + // Use custom shoutout + let channelID = await getIDByName(user) + let res = await getChannelInformation(channelID) + let channelInfo = res?.data[0] + let shoutoutMessage = `Check out ${channelInfo.broadcaster_name} over at https://twitch.tv/${channelInfo.broadcaster_login}! They were last playing ${channelInfo.game_name}` + tClient.say(channel, shoutoutMessage) + + console.log(`Shouting out ${user} in channel ${channel} for reason: ${reason}`); + + } catch (error) { + console.error('Error during shoutout:', error); + } + } + + // Handle buffered shoutout queue + async function processShoutoutQueue() { + if (soBuffer.size === 0 || isShoutoutInProgress) { + return; // Exit if no users are in the buffer or a shoutout is in progress + } + + // Set shoutout in progress flag + isShoutoutInProgress = true; + + // Get the next user in the buffer + const [user, channel] = soBuffer.entries().next().value; + + try { + await doShoutouts(channel, user); // Perform the shoutout + soBuffer.delete(user); // Remove user after shoutout is done + // console.log(`Shoutout for ${user} completed in channel ${channel}`); + } catch (error) { + console.error('Error while processing shoutout:', error); + } finally { + // Reset shoutout in progress flag and schedule next shoutout + isShoutoutInProgress = false; + setTimeout(processShoutoutQueue, 5000); // Schedule next shoutout after cooldown + } + } + + // Add user to the buffer if not already present + if (!soBuffer.has(username)) { + soBuffer.set(username, channel); + console.log(`Added ${username} to the shoutout buffer. Reason: ${reason}`); + } + + // Call the shoutout queue processing + setTimeout(processShoutoutQueue, 5000); // Ensure 5-second cooldown between shoutouts + } +}; diff --git a/events/Twitch/Subscribers.js b/events/Twitch/Subscribers.js index d72a4bb..ce37375 100644 --- a/events/Twitch/Subscribers.js +++ b/events/Twitch/Subscribers.js @@ -2,7 +2,17 @@ module.exports = { name: 'Twitch/Subscribers', once: false, - execute(stream) { - console.log(`Connected to ${stream.address}`) + async execute(channel, enabled) { + const { mClient } = require('../..') + + let db = mClient.db('shoutouts') + let col = db.collection(channel) + + // Drop Shoutout Collection to reset + try { + await col.drop() + } catch (error) { + return + } } } \ No newline at end of file diff --git a/handlers/events.js b/handlers/events.js index 71f72c4..90c4f7b 100644 --- a/handlers/events.js +++ b/handlers/events.js @@ -12,10 +12,10 @@ module.exports = (dClient) => { const filePath = path.join(eventsPath, file) const event = require(filePath) if (event.once) { - dClient.once(event.name, (...args) => event.execute(...args, dClient)) + dClient.once(event.name, (...args) => event.execute(...args)) // added client to commomerate global usage } else { - dClient.on(event.name, (...args) => event.execute(...args, dClient)) + dClient.on(event.name, (...args) => event.execute(...args)) // added client to commomerate global usage } } diff --git a/index.js b/index.js index ae50133..213741a 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const axios = require('axios'); // Validate required environment variables function validateEnvVariables() { - const requiredVars = ['MONGO_URI', 'MONGO_DB']; + const requiredVars = ['MONGO_URI']; const missingVars = requiredVars.filter((varName) => !process.env[varName]); if (missingVars.length > 0) { @@ -36,7 +36,7 @@ initializeMongoClient(); // Function to save tokens to MongoDB async function saveToken(access_token, refresh_token, expiresIn, scope) { - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const tokenData = { @@ -57,7 +57,7 @@ async function saveToken(access_token, refresh_token, expiresIn, scope) { // Function to retrieve stored tokens from MongoDB async function getStoredToken() { - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const tCreds = await credentialCollection.findOne({ service: 'twitch' }); @@ -82,7 +82,7 @@ async function refreshAccessToken() { isRefreshingToken = true; refreshPromise = (async () => { try { - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const credentials = await credentialCollection.findOne({ service: 'twitch' }); const token = await getStoredToken(); @@ -121,8 +121,8 @@ async function refreshAccessToken() { } // Function to make requests to the Twitch API -async function makeTwitchRequest(endpoint) { - const db = mClient.db(process.env.MONGO_DB); +async function makeHelixRequest(requestType, endpoint, params, data) { + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const credentials = await credentialCollection.findOne({ service: 'twitch' }); @@ -133,27 +133,77 @@ async function makeTwitchRequest(endpoint) { } try { - const response = await axios.get(`https://api.twitch.tv/helix/${endpoint}`, { - headers: { - 'Client-Id': credentials.client_id, - 'Authorization': `Bearer ${token.access_token}`, - }, - }); + var response + var headers = { + 'Client-Id': credentials.client_id, + 'Authorization': `Bearer ${token.access_token}`, + } + switch (requestType) { + case 'GET': + response = await axios.get(`https://api.twitch.tv/helix/${endpoint}`, { headers, params }); + return response.data; + break; + case 'PATCH': + response = await axios.patch(`https://api.twitch.tv/helix/${endpoint}`, data, { headers, params }) + return response.data; + break; + default: + break; + } + + - return response.data; } catch (error) { if (error.response?.status === 401) { console.log('Access token expired. Refreshing...'); const newToken = await refreshAccessToken(); if (newToken) { - return makeTwitchRequest(endpoint); // Retry with new token + return makeHelixRequest(requestType, endpoint, params, data); // Retry with new token } } console.error('Twitch API request failed:', error.response?.data || error.message); } } +async function getIDByName(user) { + // Function to convert Names to ID + let endpoint = `users` + let params = { + login: user + } + let res = await makeHelixRequest('GET', endpoint, params) + return res?.data[0]?.id +} +async function updateChatMode(broadcaster_id, setTo) { + let state + if (setTo === 'on') { + state = true + } else if (setTo === 'off') { + state = false + } else { + return + } + let db = mClient.db('clients') + let credentialCollection = db.collection('credentials') + let credentials = await credentialCollection.findOne({ service: 'twitch' }) + let endpoint = `chat/settings` + let data = { + "follower_mode": state, + "follower_mode_duration": 0, + "subscriber_mode": state, + "emote_mode": state + } + let params = { + broadcaster_id: broadcaster_id, + moderator_id: credentials.mod_id + } + makeHelixRequest('PATCH', endpoint, params, data) + +} +async function getChannelInformation(broadcaster_id){ + return response = await makeHelixRequest('GET', 'channels', { broadcaster_id: broadcaster_id }) +} // Initialize Discord Client const dClient = new Client({ intents: [ @@ -171,7 +221,7 @@ fs.readdirSync('./handlers').forEach((handler) => { // Function to log in to Discord async function dLogin() { try { - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const dCreds = await credentialCollection.findOne({ service: 'discord' }); @@ -191,7 +241,7 @@ dLogin(); // Function to log in to Twitch async function tLogin() { const channels = ['desq_blocki']; - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const tCreds = await credentialCollection.findOne({ service: 'twitch' }); @@ -246,7 +296,7 @@ async function tLogin() { console.warn('Twitch client already disconnected:', err.message); } - const db = mClient.db(process.env.MONGO_DB); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const tCreds = await credentialCollection.findOne({ service: 'twitch' }); @@ -277,4 +327,4 @@ process.on('unhandledRejection', (reason, promise) => { // Prevent app from crashing }); -module.exports = { mClient, dClient, makeTwitchRequest }; \ No newline at end of file +module.exports = { mClient, dClient, makeHelixRequest, getIDByName, updateChatMode, getChannelInformation }; \ No newline at end of file