Added Basic Discord Connectivity
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
.env
|
.env
|
||||||
TODO.txt
|
TODO.txt
|
||||||
|
assets
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ HelBot v5 introduces an improved background logic structure, streamlined integra
|
|||||||
- `user`
|
- `user`
|
||||||
- `created_at`
|
- `created_at`
|
||||||
|
|
||||||
|
**guilds.id**:
|
||||||
|
- `_id`
|
||||||
|
- `settings`
|
||||||
|
- `channels`
|
||||||
|
|
||||||
### Chat Commands:
|
### Chat Commands:
|
||||||
|
|
||||||
**Update Chat Mode** (Mods & Broadcaster only)
|
**Update Chat Mode** (Mods & Broadcaster only)
|
||||||
|
|||||||
+3
-7
@@ -3,18 +3,14 @@ const { MongoClient } = require('mongodb');
|
|||||||
const { Client, GatewayIntentBits } = require('discord.js');
|
const { Client, GatewayIntentBits } = require('discord.js');
|
||||||
const tmi = require('tmi.js');
|
const tmi = require('tmi.js');
|
||||||
const channels = ['x__hel__x', 'desq_blocki'];
|
const channels = ['x__hel__x', 'desq_blocki'];
|
||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
let mClient, dClient, tClient; // Global references
|
let mClient, dClient, tClient; // Global references
|
||||||
|
|
||||||
const initializeClients = async () => {
|
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
|
// Initialize MongoDB Client
|
||||||
mClient = new MongoClient(process.env.MONGO_URI);
|
mClient = new MongoClient(process.env.MONGO_URI);
|
||||||
await mClient.connect();
|
mClient.connect();
|
||||||
|
|
||||||
// Initialize Discord Client
|
// Initialize Discord Client
|
||||||
dClient = new 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 credentialCollection = db.collection('credentials');
|
||||||
const dCreds = await credentialCollection.findOne({ service: 'discord' });
|
const dCreds = await credentialCollection.findOne({ service: 'discord' });
|
||||||
const tCreds = await credentialCollection.findOne({ service: 'twitch' });
|
const tCreds = await credentialCollection.findOne({ service: 'twitch' });
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+158
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
+74
-19
@@ -1,23 +1,78 @@
|
|||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path');
|
||||||
module.exports = (dClient) => {
|
const { dClient, mClient, tClient } = require('..');
|
||||||
const folderPath = path.join(__dirname, '../events')
|
|
||||||
const eventFolders = fs.readdirSync(folderPath)
|
|
||||||
|
|
||||||
for (const folder of eventFolders) {
|
const folderPath = path.join(__dirname, '../events')
|
||||||
const eventsPath = path.join(folderPath, folder);
|
const eventFolders = fs.readdirSync(folderPath)
|
||||||
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'))
|
|
||||||
|
for (const folder of eventFolders) {
|
||||||
for (const file of eventFiles) {
|
const eventsPath = path.join(folderPath, folder);
|
||||||
const filePath = path.join(eventsPath, file)
|
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'))
|
||||||
const event = require(filePath)
|
|
||||||
if (event.once) {
|
for (const file of eventFiles) {
|
||||||
dClient.once(event.name, (...args) => event.execute(...args))
|
const filePath = path.join(eventsPath, file)
|
||||||
// added client to commomerate global usage
|
const event = require(filePath)
|
||||||
} else {
|
if (event.once) {
|
||||||
dClient.on(event.name, (...args) => event.execute(...args))
|
dClient.once(event.name, (...args) => event.execute(...args))
|
||||||
// added client to commomerate global usage
|
// added client to commomerate global usage
|
||||||
}
|
} else {
|
||||||
|
dClient.on(event.name, (...args) => event.execute(...args))
|
||||||
|
// added client to commomerate global usage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
// index.js
|
||||||
const { MongoClient } = require('mongodb');
|
const { MongoClient } = require('mongodb');
|
||||||
require('dotenv').config();
|
const dotenv = require('dotenv');
|
||||||
const fs = require('node:fs');
|
const fs = require('fs');
|
||||||
const { Client, GatewayIntentBits } = require('discord.js');
|
|
||||||
const tmi = require('tmi.js');
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const initializeClients = require('./clients');
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
// Validate required environment variables
|
// Validate required environment variables
|
||||||
function validateEnvVariables() {
|
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
|
// Export initialized clients
|
||||||
const mClient = new MongoClient(process.env.MONGO_URI);
|
module.exports = { mClient, dClient, tClient };
|
||||||
|
|
||||||
async function initializeMongoClient() {
|
// Load Discord event handlers
|
||||||
try {
|
fs.readdirSync('./handlers').forEach((handler) => {
|
||||||
await mClient.connect();
|
require(`./handlers/${handler}`);
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
// Prevent app from crashing
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { mClient, dClient, makeHelixRequest, getIDByName, updateChatMode, getChannelInformation };
|
|
||||||
Reference in New Issue
Block a user