diff --git a/.env.sample b/.env.sample index 5b07e3d..044ba9c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,3 @@ mongo_uri= mongo_db= -test_id= \ No newline at end of file +prefix= \ No newline at end of file diff --git a/clients.js b/clients.js new file mode 100644 index 0000000..911df90 --- /dev/null +++ b/clients.js @@ -0,0 +1,49 @@ +// clients.js +const { MongoClient } = require('mongodb'); +const { Client, GatewayIntentBits } = require('discord.js'); +const tmi = require('tmi.js'); +const channels = ['x__hel__x','desq_blocki'] + +const initializeClients = async () => { + // Initialize MongoDB Client + const mClient = new MongoClient(process.env.mongo_uri); + await mClient.connect(); + + // Initialize Discord Client + const dClient = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.MessageContent, + ], + }); + + // Fetch Discord credentials from the database + const db = mClient.db(process.env.mongo_db); + const credentialCollection = db.collection('credentials'); + const dCreds = await credentialCollection.findOne({ service: 'discord' }); + + const tCreds = await credentialCollection.findOne({ service: 'twitch' }); + + await dClient.login(dCreds.token); + + // Initialize Twitch Client + const tClient = new tmi.Client({ + options: { debug: false }, + connection: { + reconnect: true, + secure: true, + }, + identity: { + username: tCreds.username, + password: `oauth:${tCreds.token.access_token}`, + }, + channels: channels, // Replace with your channel + }); + + await tClient.connect(); + + return { mClient, dClient, tClient }; +}; + +module.exports = initializeClients; diff --git a/events/Discord/ClientReady.js b/events/Discord/ClientReady.js new file mode 100644 index 0000000..a567c2d --- /dev/null +++ b/events/Discord/ClientReady.js @@ -0,0 +1,12 @@ +const { Events, ActivityType } = require('discord.js') +module.exports = { + name: Events.ClientReady, + once: false, + execute(client) { + console.log(`Ready! Logged in as ${client.user.tag}`) + client.user.setActivity("chaos & comf", { + type: ActivityType.Streaming, + url: "https://www.twitch.tv/desq_blocki" + }); + } +} \ No newline at end of file diff --git a/events/Mongo/ClientReady.js b/events/Mongo/ClientReady.js new file mode 100644 index 0000000..37a93c0 --- /dev/null +++ b/events/Mongo/ClientReady.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'Mongo/ClientReady', + once: false, + execute(stream) { + console.log(`Connected to ${stream.address}`) + } +} \ No newline at end of file diff --git a/events/Twitch/Connected.js b/events/Twitch/Connected.js new file mode 100644 index 0000000..998d415 --- /dev/null +++ b/events/Twitch/Connected.js @@ -0,0 +1,8 @@ +// Event triggered by client connected to server +module.exports = { + name: 'Twitch/Connected', + once: false, + execute(address, port) { + console.log(`Connected to ${address}:${port}`) + } +} \ No newline at end of file diff --git a/events/Twitch/Message.js b/events/Twitch/Message.js new file mode 100644 index 0000000..ce6b93b --- /dev/null +++ b/events/Twitch/Message.js @@ -0,0 +1,106 @@ +const { dClient, mClient, tClient } = require('../..') + + +require('dotenv').configDotenv + +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) { + const result = await Helix.users.get(user); + return result.data[0].id +} +async function verifyUser(channel, username) { + let db = mClient.db("shoutouts") + let col = db.collection(channel) + + let query = { user: username } + let status = await col.findOne(query) + if (!status) { + dClient.emit('TwitchShoutout', "VIP", channel, username) + await col.insertOne({ + user: username + }) + } +} +module.exports = { + name: 'Twitch/Message', + once: false, + async execute(channel, userstate, message, self) { + if (self) { return } + if (knownBots.has(userstate.username)) { return } + + if (message.toLowerCase().includes('caw')) { + tClient.say(channel, 'CAW!') + } else if (message.toLowerCase().includes('kweh')) { + tClient.say(channel, 'KWEH!') + } else if (message.toLowerCase().includes('kaw')) { + tClient.say(channel, 'KAW!') + } + + + if (message.startsWith(process.env.prefix)) { + 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)$/ + if (!args[1].match(regex)) { break } + updateChatMode(await getIDByName(channel.substring(1)), args[1]) + break; + // case "donate": + // 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]}`) + // tClient.say(channel, `You can donate here: ${links[1]}`) + // tClient.say(channel, `Terms, conditions, rules and how rewards work are found here: ${links[2]}`) + // tClient.say(channel, `All donations and gifts are greatly appreciated, but please do so responsibly!`) + // break; + case "positivity": + let positive_Messages = ["You are so very worthy of all the love and support that may come your way.", + "There are many things which draw us together. In this time and place, that lovely subject is you, Hel.", + "The happiness and joy you add to the world is worth more than most anything.", + "You deserve every warm laugh, bright smile, and all the genuine kindness given to you.", + "There are few things as precious as your time. Thank you for sharing with us.", + "Your feelings are valid and you are not less than for having them.", + "I've seen all the work you do and I'm so very proud of you.", + "You are dearly cherished and I want to thank you for being so wonderfully you.", + "You. Are. Worthy. Enough. And I will tell you as many times as it takes for you to believe it.", + "Every day you give your best and that is so admirable. Don't let anyone tell you otherwise.", + "Your light is like that of the full moon, brightening our lives even in our darkest moments.", + "It's okay to take a moment to catch your breath. Even machines require maintenance from time to time.", + "Watching you succeed and achieve your heart's desires gives me so much motivation and I am very grateful for it."] + + let rdm = Math.floor(Math.random() * positive_Messages.length) + tClient.say(channel, positive_Messages[rdm]) + + break; + default: + break; + } + } + // ---------------VIP Handler------------------- + if (!userstate.badges?.vip) { return } + verifyUser(channel, userstate.username) + // --------------------------------------------- + } + +} \ No newline at end of file diff --git a/events/Twitch/Raided.js b/events/Twitch/Raided.js new file mode 100644 index 0000000..0ffe02d --- /dev/null +++ b/events/Twitch/Raided.js @@ -0,0 +1,8 @@ +// Event triggered by Raid +module.exports = { + name: 'Twitch/Raided', + once: false, + execute(stream) { + console.log(`Connected to ${stream.address}`) + } +} \ No newline at end of file diff --git a/events/Twitch/Subscribers.js b/events/Twitch/Subscribers.js new file mode 100644 index 0000000..d72a4bb --- /dev/null +++ b/events/Twitch/Subscribers.js @@ -0,0 +1,8 @@ +// Event triggered by SubscribersOnly mode +module.exports = { + name: 'Twitch/Subscribers', + once: false, + execute(stream) { + console.log(`Connected to ${stream.address}`) + } +} \ No newline at end of file diff --git a/events/TwitchOnMessage.js b/events/TwitchOnMessage.js deleted file mode 100644 index e69de29..0000000 diff --git a/handlers/commands.js b/handlers/commands.js deleted file mode 100644 index e69de29..0000000 diff --git a/handlers/events.js b/handlers/events.js index e69de29..71f72c4 100644 --- a/handlers/events.js +++ b/handlers/events.js @@ -0,0 +1,23 @@ +const fs = require('node:fs') +const path = require('node:path') +module.exports = (dClient) => { + const folderPath = path.join(__dirname, '../events') + const eventFolders = fs.readdirSync(folderPath) + + for (const folder of eventFolders) { + const eventsPath = path.join(folderPath, folder); + const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')) + + for (const file of eventFiles) { + const filePath = path.join(eventsPath, file) + const event = require(filePath) + if (event.once) { + dClient.once(event.name, (...args) => event.execute(...args, dClient)) + // added client to commomerate global usage + } else { + dClient.on(event.name, (...args) => event.execute(...args, dClient)) + // added client to commomerate global usage + } + } + } +} \ No newline at end of file diff --git a/index.js b/index.js index b7c056f..93f12d8 100644 --- a/index.js +++ b/index.js @@ -1,93 +1,132 @@ const { MongoClient } = require('mongodb'); -require('dotenv').config() -const mClient = new MongoClient(process.env.mongo_uri) -exports.mClient = mClient; +require('dotenv').config(); +const fs = require('node:fs'); +const { Client, GatewayIntentBits } = require('discord.js'); +const tmi = require('tmi.js'); +const axios = require('axios'); +// Validate required environment variables +function validateEnvVariables() { + const requiredVars = ['MONGO_URI', 'MONGO_DB']; + const missingVars = requiredVars.filter((varName) => !process.env[varName]); -async function connectToMongo() { - await mClient.connect(); - console.log('Connected to MongoDB'); - - async function getChannelInformation(broadcasterId) { - const endpoint = `channels?broadcaster_id=${broadcasterId}`; - const channelInfo = await makeTwitchRequest(endpoint); - if (channelInfo) { - console.log('Channel Information:', channelInfo); - } else { - console.log('Failed to retrieve channel information.'); - } + if (missingVars.length > 0) { + console.error(`Missing required environment variables: ${missingVars.join(', ')}`); + process.exit(1); } - getChannelInformation(process.env.test_id); - } -connectToMongo(); -async function saveToken(accessToken, refreshToken, expires_in) { - let db = mClient.db(process.env.mongo_db); - let tokenCollection = db.collection("twitch-token"); +validateEnvVariables(); + +// Initialize MongoDB Client +const mClient = new MongoClient(process.env.MONGO_URI); + +async function initializeMongoClient() { + try { + await mClient.connect(); + console.log('MongoDB connected successfully.'); + // Emit event or proceed with dependent initializations here + } catch (error) { + console.error('Failed to connect to MongoDB:', error); + process.exit(1); // Exit the process if the connection fails + } +} + +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 credentialCollection = db.collection('credentials'); const tokenData = { - accessToken, - refreshToken, - expires_in, - obtainedAt: Date.now() + access_token, + refresh_token, + scope, + expiresIn, + obtainedAt: Date.now(), }; - await tokenCollection.updateOne({}, { $set: tokenData }, { upsert: true }); - console.log('TTV Token updated in MongoDB'); + await credentialCollection.updateOne( + { service: 'twitch' }, + { $set: { token: tokenData } }, + { upsert: true } + ); + console.log('Twitch token updated in MongoDB'); } -async function getStoredToken() { - let db = mClient.db(process.env.mongo_db); - let tokenCollection = db.collection("twitch-token"); - const tokenData = await tokenCollection.findOne({}); - if (tokenData) { +// Function to retrieve stored tokens from MongoDB +async function getStoredToken() { + const db = mClient.db(process.env.MONGO_DB); + const credentialCollection = db.collection('credentials'); + + const tCreds = await credentialCollection.findOne({ service: 'twitch' }); + if (tCreds) { console.log('Loaded token from MongoDB'); - return tokenData; + return tCreds.token; } else { console.log('No token found in MongoDB'); return null; } } -const axios = require('axios'); +// Function to refresh the access token +let isRefreshingToken = false; +let refreshPromise = null; + async function refreshAccessToken() { - let db = mClient.db(process.env.mongo_db) - let credentialCollection = db.collection('credentials') - let credentials = await credentialCollection.find({ service: 'twitch' }).toArray() + if (isRefreshingToken) { + return refreshPromise; // Return the existing refresh promise if a refresh is already in progress + } + + isRefreshingToken = true; + refreshPromise = (async () => { + try { + const db = mClient.db(process.env.MONGO_DB); + const credentialCollection = db.collection('credentials'); + const credentials = await credentialCollection.findOne({ service: 'twitch' }); + const token = await getStoredToken(); + if (!token) { + throw new Error('No token found! Please authenticate manually.'); + } + + const response = await axios.post('https://id.twitch.tv/oauth2/token', null, { + params: { + grant_type: 'refresh_token', + refresh_token: token.refresh_token, + client_id: credentials.client_id, + scope: token.scope, + client_secret: credentials.client_secret, + }, + }); + + const newAccessToken = response.data.access_token; + const newRefreshToken = response.data.refresh_token || token.refresh_token; + const newExpirationDate = response.data.expires_in || 0; + + await saveToken(newAccessToken, newRefreshToken, newExpirationDate, token.scope); + console.log('Access token refreshed successfully.'); + + return newAccessToken; + } catch (error) { + console.error('Failed to refresh access token:', error.response?.data || error.message); + throw error; + } finally { + isRefreshingToken = false; + refreshPromise = null; + } + })(); + + return refreshPromise; +} + +// Function to make requests to the Twitch API +async function makeTwitchRequest(endpoint) { + const db = mClient.db(process.env.MONGO_DB); + const credentialCollection = db.collection('credentials'); + const credentials = await credentialCollection.findOne({ service: 'twitch' }); const token = await getStoredToken(); - if (!token) { - console.error('No token found! Please authenticate manually.'); - return; - } - - try { - const response = await axios.post('https://id.twitch.tv/oauth2/token', null, { - params: { - grant_type: 'refresh_token', - refresh_token: token.refreshToken, - client_id: credentials.client_id, - client_secret: credentials.client_secret - } - }); - - const newAccessToken = response.data.access_token; - const newRefreshToken = response.data.refresh_token || token.refreshToken; - const newExpirationDate = response.data.expires_in || 0 - - await saveToken(newAccessToken, newRefreshToken, newExpirationDate); - console.log('Access token refreshed successfully'); - } catch (error) { - console.error('Failed to refresh access token:', error.response?.data || error.message); - } -} -async function makeTwitchRequest(endpoint) { - let db = mClient.db(process.env.mongo_db) - let credentialCollection = db.collection('credentials') - let credentials = await credentialCollection.findOne({ service: 'twitch' }) - - let token = await getStoredToken(); if (!token.access_token) { console.error('No access token available.'); return; @@ -97,14 +136,13 @@ async function makeTwitchRequest(endpoint) { const response = await axios.get(`https://api.twitch.tv/helix/${endpoint}`, { headers: { 'Client-Id': credentials.client_id, - 'Authorization': `Bearer ${token.access_token}` - } + 'Authorization': `Bearer ${token.access_token}`, + }, }); return response.data; } catch (error) { if (error.response?.status === 401) { - console.log(error) console.log('Access token expired. Refreshing...'); const newToken = await refreshAccessToken(); if (newToken) { @@ -114,4 +152,99 @@ async function makeTwitchRequest(endpoint) { console.error('Twitch API request failed:', error.response?.data || error.message); } -} \ No newline at end of file +} + +// Initialize Discord Client +const dClient = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.MessageContent, + ], +}); +exports.dClient = dClient; + +// Load Discord event handlers +fs.readdirSync('./handlers').forEach((handler) => { + require(`./handlers/${handler}`)(dClient); +}); + +// Function to log in to Discord +async function dLogin() { + try { + const db = mClient.db(process.env.MONGO_DB); + const credentialCollection = db.collection('credentials'); + const dCreds = await credentialCollection.findOne({ service: 'discord' }); + + if (!dCreds || !dCreds.token) { + throw new Error('Discord credentials not found.'); + } + + await dClient.login(dCreds.token); + console.log('Discord client logged in successfully.'); + } catch (error) { + console.error('Discord login failed:', error); + } +} + +dLogin(); + +// Function to log in to Twitch +async function tLogin() { + const channels = ['desq_blocki']; + const db = mClient.db(process.env.MONGO_DB); + const credentialCollection = db.collection('credentials'); + const tCreds = await credentialCollection.findOne({ service: 'twitch' }); + + if (!tCreds || !tCreds.username || !tCreds.token || !tCreds.token.access_token) { + console.error('Twitch credentials are missing or incomplete.'); + return; + } + + const tClient = new tmi.Client({ + options: { debug: false }, + connection: { + reconnect: true, + secure: true, + }, + identity: { + username: tCreds.username, + password: `oauth:${tCreds.token.access_token}`, + }, + channels: channels, + }); + + tClient.connect(); + + + // Handle Twitch events + tClient.on('connected', (address, port) => { + dClient.emit('Twitch/Connected', address, port); + }); + + tClient.on('message', async (channel, userstate, message, self) => { + dClient.emit('Twitch/Message', channel, userstate, message, self); + }); + + tClient.on('raided', (channel, username, viewers) => { + dClient.emit('Twitch/Raided', channel, username, viewers); + }); + + tClient.on('subscribers', (channel, enabled) => { + dClient.emit('Twitch/Subscribers', channel, enabled); + }); + + tClient.on('disconnected', (reason) => { + console.log('Twitch client disconnected:', reason); + if (reason.includes('Login authentication failed')) { + refreshAccessToken() + .then(() => tClient.connect()) + .catch((error) => { + console.error('Failed to refresh token and reconnect:', error); + }); + } + }); +} + +tLogin(); +