From bd782b0567e35ea4fabea970c61ee3191ef900cd Mon Sep 17 00:00:00 2001 From: DeSqBlocki Date: Sat, 8 Feb 2025 16:47:07 +0100 Subject: [PATCH] Added Basic Discord Connectivity --- .gitignore | 3 +- README.md | 5 + clients.js | 10 +- commands/discord/simJoin.js | 39 +++ commands/discord/simLive.js | 23 ++ commands/discord/simShoutout.js | 42 +++ commands/discord/socials.js | 66 ++++ commands/{chatmode.js => mongo/settings.js} | 0 commands/twitch/chatmode.js | 0 events/Discord/InteractionCreate.js | 26 ++ functions.js | 158 ++++++++++ handlers/commands.js | 57 ++++ handlers/events.js | 93 ++++-- index.js | 322 +------------------- 14 files changed, 511 insertions(+), 333 deletions(-) create mode 100644 commands/discord/simJoin.js create mode 100644 commands/discord/simLive.js create mode 100644 commands/discord/simShoutout.js create mode 100644 commands/discord/socials.js rename commands/{chatmode.js => mongo/settings.js} (100%) create mode 100644 commands/twitch/chatmode.js create mode 100644 events/Discord/InteractionCreate.js create mode 100644 functions.js create mode 100644 handlers/commands.js diff --git a/.gitignore b/.gitignore index 5596c37..50f4b4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env -TODO.txt \ No newline at end of file +TODO.txt +assets diff --git a/README.md b/README.md index 4339144..6ecbdd0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ HelBot v5 introduces an improved background logic structure, streamlined integra - `user` - `created_at` +**guilds.id**: +- `_id` +- `settings` +- `channels` + ### Chat Commands: **Update Chat Mode** (Mods & Broadcaster only) diff --git a/clients.js b/clients.js index 6b641a4..9edf97c 100644 --- a/clients.js +++ b/clients.js @@ -3,18 +3,14 @@ const { MongoClient } = require('mongodb'); const { Client, GatewayIntentBits } = require('discord.js'); const tmi = require('tmi.js'); const channels = ['x__hel__x', 'desq_blocki']; +require('dotenv').config() let mClient, dClient, tClient; // Global references const initializeClients = async () => { - // Disconnect existing clients if they exist - if (tClient) await tClient.disconnect().catch(() => {}); - if (dClient) await dClient.destroy().catch(() => {}); - if (mClient) await mClient.close().catch(() => {}); - // Initialize MongoDB Client mClient = new MongoClient(process.env.MONGO_URI); - await mClient.connect(); + mClient.connect(); // Initialize Discord Client dClient = new Client({ @@ -25,7 +21,7 @@ const initializeClients = async () => { ], }); - const db = mClient.db("client"); + const db = mClient.db("clients"); const credentialCollection = db.collection('credentials'); const dCreds = await credentialCollection.findOne({ service: 'discord' }); const tCreds = await credentialCollection.findOne({ service: 'twitch' }); diff --git a/commands/discord/simJoin.js b/commands/discord/simJoin.js new file mode 100644 index 0000000..5876bbb --- /dev/null +++ b/commands/discord/simJoin.js @@ -0,0 +1,39 @@ +const { SlashCommandBuilder, Events } = require('discord.js') +module.exports = { + data: new SlashCommandBuilder() + .setName('simjoin') + .setDescription('simulates a join event') + .addMentionableOption((option) => + option.setName('target').setDescription('user')), + async execute(interaction) { + + if(!interaction.member.permissions.has("ADMINISTRATOR")){ + return await interaction.reply({ + content: "Unprivileged Access!", + ephemeral: true + }) + } + + let target = interaction.options.getMentionable('target') + if(!target){ + target = interaction.member + } + try { + if(target.user.username){ + interaction.client.emit(Events.GuildMemberAdd, target) + await interaction.reply({ + content: "Done!", + ephemeral: true + }) + } + } catch (error) { + await interaction.reply({ + content: "Invalid Mentionable!", + ephemeral: true + }) + } + + + + } +} \ No newline at end of file diff --git a/commands/discord/simLive.js b/commands/discord/simLive.js new file mode 100644 index 0000000..3eaf057 --- /dev/null +++ b/commands/discord/simLive.js @@ -0,0 +1,23 @@ +const { SlashCommandBuilder, Events } = require('discord.js') + +module.exports = { + data: new SlashCommandBuilder() + .setName('simlive') + .setDescription('simulates a live event'), + async execute(interaction) { + if(!interaction.member.permissions.has("ADMINISTRATOR")){ + return await interaction.reply({ + content: "Unprivileged Access!", + ephemeral: true + }) + } + + const streamer = 'x__hel__x' + interaction.client.emit('TwitchLive', streamer) + await interaction.reply({ + content: "Done!", + ephemeral: true + }) + + } +} \ No newline at end of file diff --git a/commands/discord/simShoutout.js b/commands/discord/simShoutout.js new file mode 100644 index 0000000..f8e2a59 --- /dev/null +++ b/commands/discord/simShoutout.js @@ -0,0 +1,42 @@ +const { SlashCommandBuilder } = require('discord.js') +const { mClient } = require('../..') + +module.exports = { + data: new SlashCommandBuilder() + .setName('simshoutout') + .setDescription('simulates a shoutout event'), + async execute(interaction) { + await interaction.deferReply() + if (!interaction.member.permissions.has("ADMINISTRATOR")) { + return await interaction.editReply({ + content: "Unprivileged Access!", + ephemeral: true + }) + } + let channel = '#desq_blocki' + let username = 'desq_blocki' + + let db = mClient.db("shoutouts") + let col = db.collection(channel) + + let query = { user: username } + let status = await col.findOne(query) + if (!status) { + interaction.client.emit("TwitchShoutout", "TEST", channel, username) + await col.insertOne({ + user: username + }) + await interaction.editReply({ + content: "Done!", + ephemeral: true + }) + } else { + await interaction.editReply({ + content: "Already in Database", + ephemeral: true + }) + } + + + } +} \ No newline at end of file diff --git a/commands/discord/socials.js b/commands/discord/socials.js new file mode 100644 index 0000000..e244deb --- /dev/null +++ b/commands/discord/socials.js @@ -0,0 +1,66 @@ +const { SlashCommandBuilder, EmbedBuilder, ButtonBuilder } = require('discord.js') +module.exports = { + data: new SlashCommandBuilder() + .setName('socials') + .setDescription('links to social media'), + async execute(interaction) { + await interaction.reply({ + "components": [ + { + "type": 1, + "components": [ + { + "style": 5, + "label": `Twitch`, + "url": `https://www.twitch.tv/x__hel__x`, + "disabled": false, + "emoji": { + "id": `1136026329091952740`, + "name": `Twitch`, + "animated": false + }, + "type": 2 + }, + { + "style": 5, + "label": `Twitter`, + "url": `https://twitter.com/x__Hel__x`, + "disabled": false, + "emoji": { + "id": `1136026228093095946`, + "name": `Twitter`, + "animated": false + }, + "type": 2 + }, + { + "style": 5, + "label": `Ko-fi`, + "url": `https://ko-fi.com/hel4444/commissions`, + "disabled": false, + "emoji": { + "id": `1136027117428150312`, + "name": `Kofi`, + "animated": false + }, + "type": 2 + } + ] + } + ], + "embeds": [ + { + "type": "rich", + "title": `Social Media Links`, + "description": `Click on one of these buttons to get to Hel's social media pages.`, + "color": 0xdda807, + "thumbnail": { + "url": `https://cdn.discordapp.com/avatars/901259181397528678/13796c7450e586611042f7c2c72684f7.png?size=4096`, + "height": 0, + "width": 0 + } + } + ] + }) + } +} \ No newline at end of file diff --git a/commands/chatmode.js b/commands/mongo/settings.js similarity index 100% rename from commands/chatmode.js rename to commands/mongo/settings.js diff --git a/commands/twitch/chatmode.js b/commands/twitch/chatmode.js new file mode 100644 index 0000000..e69de29 diff --git a/events/Discord/InteractionCreate.js b/events/Discord/InteractionCreate.js new file mode 100644 index 0000000..3c0ac23 --- /dev/null +++ b/events/Discord/InteractionCreate.js @@ -0,0 +1,26 @@ +const { Events } = require('discord.js') + +module.exports = { + name: Events.InteractionCreate, + once: false, + async execute(interaction) { + if (!interaction.isChatInputCommand()) return; + + const command = interaction.client.commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + try { + await command.execute(interaction); + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); + } else { + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } + } + } +} \ No newline at end of file diff --git a/functions.js b/functions.js new file mode 100644 index 0000000..f9452f4 --- /dev/null +++ b/functions.js @@ -0,0 +1,158 @@ +// Delay function for pauses +function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } + +// Save token to MongoDB +async function saveToken(access_token, refresh_token, expiresIn, scope) { + const db = mClient.db("clients"); + const credentialCollection = db.collection('credentials'); + + const tokenData = { + access_token, + refresh_token, + scope, + expiresIn, + obtainedAt: Date.now(), + }; + + await credentialCollection.updateOne( + { service: 'twitch' }, + { $set: { token: tokenData } }, + { upsert: true } + ); + console.log('Twitch token updated in MongoDB'); +} + +// Retrieve stored token from MongoDB +async function getStoredToken() { + const db = mClient.db("clients"); + const credentialCollection = db.collection('credentials'); + + const tCreds = await credentialCollection.findOne({ service: 'twitch' }); + if (tCreds) { + console.log('Loaded token from MongoDB'); + return tCreds.token; + } + console.log('No token found in MongoDB'); + return null; +} + +// Refresh the access token +let isRefreshingToken = false; +let refreshPromise = null; + +async function refreshAccessToken() { + if (isRefreshingToken) { + return refreshPromise; // Return existing refresh promise + } + + isRefreshingToken = true; + refreshPromise = (async () => { + try { + const db = mClient.db("clients"); + 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 { access_token, refresh_token, expires_in } = response.data; + + await saveToken(access_token, refresh_token || token.refresh_token, expires_in, token.scope); + console.log('Access token refreshed successfully.'); + + return access_token; + } catch (error) { + console.error('Failed to refresh access token:', error.response?.data || error.message); + throw error; + } finally { + isRefreshingToken = false; + refreshPromise = null; + } + })(); + + return refreshPromise; +} + +// Make requests to the Twitch API +async function makeHelixRequest(requestType, endpoint, params, data) { + const db = mClient.db("clients"); + const credentialCollection = db.collection('credentials'); + const credentials = await credentialCollection.findOne({ service: 'twitch' }); + + const token = await getStoredToken(); + if (!token?.access_token) { + console.error('No access token available.'); + return; + } + + try { + const headers = { + 'Client-Id': credentials.client_id, + 'Authorization': `Bearer ${token.access_token}`, + }; + + let response; + switch (requestType) { + case 'GET': + response = await axios.get(`https://api.twitch.tv/helix/${endpoint}`, { headers, params }); + return response.data; + case 'PATCH': + response = await axios.patch(`https://api.twitch.tv/helix/${endpoint}`, data, { headers, params }); + return response.data; + default: + throw new Error('Invalid request type'); + } + } catch (error) { + if (error.response?.status === 401) { + console.log('Access token expired. Refreshing...'); + const newToken = await refreshAccessToken(); + if (newToken) { + return makeHelixRequest(requestType, endpoint, params, data); // Retry with new token + } + } + + console.error('Twitch API request failed:', error.response?.data || error.message); + } +} + +// Convert Twitch username to user ID +async function getIDByName(user) { + const params = { login: user }; + const response = await makeHelixRequest('GET', 'users', params); + return response?.data[0]?.id; +} + +// Update chat mode (follower/subscriber mode) +async function updateChatMode(broadcaster_id, setTo) { + const state = (setTo === 'on'); + const db = mClient.db('clients'); + const credentialCollection = db.collection('credentials'); + const credentials = await credentialCollection.findOne({ service: 'twitch' }); + + const data = { + "follower_mode": state, + "follower_mode_duration": 0, + "subscriber_mode": state, + "emote_mode": state + }; + const params = { + broadcaster_id, + moderator_id: credentials.mod_id + }; + + await makeHelixRequest('PATCH', 'chat/settings', params, data); +} + +// Get channel information +async function getChannelInformation(broadcaster_id) { + return await makeHelixRequest('GET', 'channels', { broadcaster_id }); +} \ No newline at end of file diff --git a/handlers/commands.js b/handlers/commands.js new file mode 100644 index 0000000..9aa7cd1 --- /dev/null +++ b/handlers/commands.js @@ -0,0 +1,57 @@ +const { REST, Routes, Collection } = require('discord.js'); +const { configDotenv } = require('dotenv'); +configDotenv +const fs = require('node:fs'); +const path = require('node:path'); +const { mClient, dClient } = require('..'); + + +dClient.commands = new Collection() +const commands = []; +// Grab all the command folders from the commands directory you created earlier +const foldersPath = path.join(__dirname, '../commands'); +const commandFolders = fs.readdirSync(foldersPath); + +for (const folder of commandFolders) { + // Grab all the command files from the commands directory you created earlier + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ('data' in command && 'execute' in command) { + commands.push(command.data.toJSON()); + dClient.commands.set(command.data.name, command); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } +} + + + + +(async () => { + const credDb = mClient.db('clients') + const credColl = credDb.collection('credentials') + const credentials = await credColl.findOne({ service: 'discord' }) + + // Construct and prepare an instance of the REST module + const rest = new REST().setToken(credentials.token); + + // and deploy your commands! + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationCommands(credentials.client_id), + { body: commands }, + ); + + console.log(`Successfully reloaded ${data.length} application (/) commands.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } +})(); \ No newline at end of file diff --git a/handlers/events.js b/handlers/events.js index 90c4f7b..77ebbec 100644 --- a/handlers/events.js +++ b/handlers/events.js @@ -1,23 +1,78 @@ const fs = require('node:fs') -const path = require('node:path') -module.exports = (dClient) => { - const folderPath = path.join(__dirname, '../events') - const eventFolders = fs.readdirSync(folderPath) +const path = require('node:path'); +const { dClient, mClient, tClient } = require('..'); - 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)) - // added client to commomerate global usage - } else { - dClient.on(event.name, (...args) => event.execute(...args)) - // added client to commomerate global usage - } +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)) + // added client to commomerate global usage + } else { + dClient.on(event.name, (...args) => event.execute(...args)) + // added client to commomerate global usage } } -} \ No newline at end of file +} + +// Custom Twitch Events via dClient + +// Handle Twitch events +tClient.on('connected', (address, port) => { + dClient.emit('Twitch/Connected', address, port, tClient); +}); + +tClient.on('message', async (channel, userstate, message, self) => { + dClient.emit('Twitch/Message', channel, userstate, message, self, tClient); +}); + +tClient.on('raided', (channel, username, viewers) => { + dClient.emit('Twitch/Raided', channel, username, viewers, tClient); +}); + +tClient.on('subscribers', (channel, enabled) => { + dClient.emit('Twitch/Subscribers', channel, enabled, tClient); +}); + +tClient.on('disconnected', async (reason) => { + console.log('Twitch disconnected:', reason); + if (reason.includes('Login authentication failed')) { + try { + await refreshAccessToken(); + console.log('Reinitializing Twitch client after token refresh...'); + + try { + await tClient.disconnect(); + } catch (err) { + console.warn('Twitch client already disconnected:', err.message); + } + + const db = mClient.db("clients"); + const credentialCollection = db.collection('credentials'); + const tCreds = await credentialCollection.findOne({ service: 'twitch' }); + + // Recreate the client + tClient = new tmi.Client({ + options: { debug: false }, + connection: { reconnect: true, secure: true }, + identity: { + username: tCreds.username, + password: `oauth:${tCreds.token.access_token}`, + }, + channels: channels, + }); + + await tClient.connect(); + console.log('Twitch client reconnected successfully.'); + } catch (error) { + console.error('Failed to refresh token and reconnect:', error); + } + } +}); \ No newline at end of file diff --git a/index.js b/index.js index 213741a..7be25a4 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,11 @@ +// index.js const { MongoClient } = require('mongodb'); -require('dotenv').config(); -const fs = require('node:fs'); -const { Client, GatewayIntentBits } = require('discord.js'); -const tmi = require('tmi.js'); +const dotenv = require('dotenv'); +const fs = require('fs'); const axios = require('axios'); +const initializeClients = require('./clients'); + +dotenv.config(); // Validate required environment variables function validateEnvVariables() { @@ -16,315 +18,23 @@ function validateEnvVariables() { } } -validateEnvVariables(); +// Initialize clients asynchronously and export them +async function startApp() { + const { mClient, dClient, tClient } = await initializeClients(); + validateEnvVariables(); -// Initialize MongoDB Client -const mClient = new MongoClient(process.env.MONGO_URI); + // Export initialized clients + module.exports = { mClient, dClient, tClient }; -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("clients"); - const credentialCollection = db.collection('credentials'); - - const tokenData = { - access_token, - refresh_token, - scope, - expiresIn, - obtainedAt: Date.now(), - }; - - await credentialCollection.updateOne( - { service: 'twitch' }, - { $set: { token: tokenData } }, - { upsert: true } - ); - console.log('Twitch token updated in MongoDB'); -} - -// Function to retrieve stored tokens from MongoDB -async function getStoredToken() { - const db = mClient.db("clients"); - const credentialCollection = db.collection('credentials'); - - const tCreds = await credentialCollection.findOne({ service: 'twitch' }); - if (tCreds) { - console.log('Loaded token from MongoDB'); - return tCreds.token; - } else { - console.log('No token found in MongoDB'); - return null; - } -} - -// Function to refresh the access token -let isRefreshingToken = false; -let refreshPromise = null; - -async function refreshAccessToken() { - 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("clients"); - 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 makeHelixRequest(requestType, endpoint, params, data) { - const db = mClient.db("clients"); - const credentialCollection = db.collection('credentials'); - const credentials = await credentialCollection.findOne({ service: 'twitch' }); - - const token = await getStoredToken(); - if (!token.access_token) { - console.error('No access token available.'); - return; - } - - try { - 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; - } - - - - } catch (error) { - if (error.response?.status === 401) { - console.log('Access token expired. Refreshing...'); - const newToken = await refreshAccessToken(); - if (newToken) { - 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: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.MessageContent, - ], -}); - -// 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("clients"); - 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("clients"); - 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; - } - - var tClient = new tmi.Client({ - options: { debug: false }, - connection: { - reconnect: true, - secure: true, - }, - identity: { - username: tCreds.username, - password: `oauth:${tCreds.token.access_token}`, - }, - channels: channels, + // Load Discord event handlers + fs.readdirSync('./handlers').forEach((handler) => { + require(`./handlers/${handler}`); }); - tClient.connect(); - - - // Handle Twitch events - tClient.on('connected', (address, port) => { - dClient.emit('Twitch/Connected', address, port, tClient); - }); - - tClient.on('message', async (channel, userstate, message, self) => { - dClient.emit('Twitch/Message', channel, userstate, message, self, tClient); - }); - - tClient.on('raided', (channel, username, viewers) => { - dClient.emit('Twitch/Raided', channel, username, viewers, tClient); - }); - - tClient.on('subscribers', (channel, enabled) => { - dClient.emit('Twitch/Subscribers', channel, enabled, tClient); - }); - - tClient.on('disconnected', async (reason) => { - console.log('Twitch disconnected:', reason); - if (reason.includes('Login authentication failed')) { - try { - await refreshAccessToken(); - console.log('Reinitializing Twitch client after token refresh...'); - - try { - await tClient.disconnect(); - } catch (err) { - console.warn('Twitch client already disconnected:', err.message); - } - - const db = mClient.db("clients"); - const credentialCollection = db.collection('credentials'); - const tCreds = await credentialCollection.findOne({ service: 'twitch' }); - - // Recreate the client - tClient = new tmi.Client({ - options: { debug: false }, - connection: { reconnect: true, secure: true }, - identity: { - username: tCreds.username, - password: `oauth:${tCreds.token.access_token}`, - }, - channels: channels, - }); - - await tClient.connect(); - console.log('Twitch client reconnected successfully.'); - } catch (error) { - console.error('Failed to refresh token and reconnect:', error); - } - } - }); } -tLogin(); +startApp().catch(console.error); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); - // Prevent app from crashing }); - -module.exports = { mClient, dClient, makeHelixRequest, getIDByName, updateChatMode, getChannelInformation }; \ No newline at end of file