first commit

This commit is contained in:
2025-06-21 15:13:58 -05:00
commit 07f75bbd93
37 changed files with 3125 additions and 0 deletions

49
.eslintrc.json Normal file
View File

@ -0,0 +1,49 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
.env
config.json
dockerfile

92
commands/misc/pinguser.js Normal file
View File

@ -0,0 +1,92 @@
const { Client, Interaction, SlashCommandBuilder } = require("discord.js");
const cooldowns = new Map();
var isDisabled = false;
/**
* Handles the ping user command.
* @param {Interaction} interaction - The interaction object.
*/
async function handlePingCommand(interaction) {
const user = interaction.options.getUser("user"); // Get the user from the interaction
const times = interaction.options.getInteger("times") || 1; // Default to 1 if not provided
try {
// Ensure the user exists in the guild
const userPing = await interaction.guild.members.fetch(user.id);
if (!userPing) {
await interaction.reply("That user doesn't exist in this server.");
return;
}
// Prevent the bot from pinging itself
if (userPing.id === interaction.guild.members.me.id) {
await interaction.reply("I can't ping myself.");
return;
}
// Cooldown logic
const coolDown = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const now = Date.now();
const lastExec = cooldowns.get(interaction.user.id);
if (lastExec && now - lastExec < coolDown) {
const timeLeft = coolDown - (now - lastExec);
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
await interaction.reply(
`You need to wait ${days} days, ${hours} hours, ${minutes} minutes, and ${seconds} seconds before using this command again.`,
);
return;
}
// Send the pings
await interaction.reply({ content: "Ok, sending pings.", ephemeral: false });
for (let i = 0; i < times; i++) {
await interaction.channel.send(`Pinging ${userPing}`);
await new Promise((resolve) => setTimeout(resolve, 300)); // Wait 300ms between pings
}
// Update the cooldown
cooldowns.set(interaction.user.id, now);
} catch (error) {
console.error("There was an error in pinguser: ", error);
await interaction.reply({
content: "An error occurred while executing the command.",
ephemeral: true,
});
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName("pinguser")
.setDescription("Ping the mentioned user")
.addUserOption((option) =>
option.setName("user").setDescription("The user to ping").setRequired(true),
)
.addIntegerOption((option) =>
option
.setName("times")
.setDescription("Number of times to ping the user")
.setRequired(false),
),
async execute(interaction) {
try {
if (isDisabled) {
return interaction.reply({
content: "The command is disabled... Try again later.",
ephemeral: true,
});
}
await handlePingCommand(interaction); // Ensure we await this function
} catch (error) {
console.error("There was an error executing the command: ", error);
}
},
};

View File

@ -0,0 +1,92 @@
const {
Client,
Interaction,
ApplicationCommandOptionType,
PermissionFlagsBits,
SlashCommandBuilder,
} = require("discord.js");
async function handleBanCommand(interaction) {
if (!interaction.member.permissions.has(PermissionFlagsBits.BanMembers)) {
await interaction.reply({
content: "You do not have permission to ban members.",
ephemeral: true,
});
return;
}
const targetID = interaction.options.get("user").value;
const reason =
interaction.options.get("reason")?.value || "No reason provided";
await interaction.deferReply();
const targetUser = await interaction.guild.members.fetch(targetID);
if (!targetUser) {
await interaction.editReply("That user is not in the server");
return;
}
if (targetUser.id === interaction.guild.ownerId) {
await interaction.editReply("I can't ban the server owner");
return;
}
if (targetUser.id === interaction.guild.members.me) {
await interaction.editReply("I cant't ban myself");
return;
}
const targetUserRolePostion = targetUser.roles.highest.postion; // check the user highest role
const requestUserRolePostion = interaction.member.roles.highest.postion; // check the user issuing the command is higher than the target
const botRolePostion = interaction.guild.members.me.roles.highest.postion; // check the bot has permissions
if (targetUserRolePostion >= requestUserRolePostion) {
await interaction.editReply(
"You can't ban someone that has the same/higher role than you."
);
return;
}
if (targetUserRolePostion >= botRolePostion) {
await interaction.editReply(
"I can't ban that user because they have the same/higher role than me."
);
return;
}
try {
await targetUser.ban({ reason });
await interaction.editReply(
`User ${targetUser} was banned. Reason: ${reason}`
);
} catch (error) {
console.error("There was an error banning the target: ", error);
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName("ban")
.setDescription("Bans the specified user")
.addUserOption((option) =>
option
.setName("user")
.setDescription("Mention the user to ban")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("Specify the reason for banning")
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction) {
try {
handleBanCommand(interaction);
} catch (error) {
console.error("There was an error in banning: ", error);
}
},
};

View File

@ -0,0 +1,75 @@
const {
Client,
Interaction,
ApplicationCommandOptionType,
PermissionFlagsBits,
SlashCommandBuilder,
} = require("discord.js");
async function handleKickCommand(interaction) {
if (!interaction.member.permissions.has(PermissionFlagsBits.KickMembers)) {
await interaction.reply({
content: "You do not have permission to kick members.",
ephemeral: true,
});
return;
}
const targetID = interaction.options.get('user').value;
const reason = interaction.options.get("reason")?.value || "No reason provided";
await interaction.deferReply();
const targetUser = await interaction.guild.members.fetch(targetID)
if (!targetUser) {
await interaction.editReply('That user does not exist in this server');
return;
}
if (targetUser.id === interaction.guild.ownerId) {
await interaction.editReply("I can't kick the server owner")
return;
}
if (targetUser.id === interaction.guild.members.me) {
await interaction.editReply("I can't kick myself")
return;
}
const targetUserRolePostion = targetUser.roles.highest.postion;
const requestUserRolePostion = interaction.member.roles.highest.postion
const botRolePostion = interaction.guild.members.me.roles.highest.postion
if (targetUserRolePostion >= requestUserRolePostion) {
await interaction.editReply("You can't kick someone that has the same/higher role than you")
return;
}
if (targetUserRolePostion >= botRolePostion) {
await interaction.editReply("I can't kick that user because they have the same/higher role than me.")
return;
}
try {
await targetUser.kick({ reason })
await interaction.editReply(`User ${targetUser} was kicked. Reason: ${reason}`)
} catch (error) {
console.error('There was a problem kicking: ', error)
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName("kick")
.setDescription("Kicks the specified user")
.addUserOption((option) => option.setName("user").setDescription("Mention the user to kick").setRequired(true))
.addStringOption((option) => option.setName('reason').setDescription('Specify the reason for kicking').setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
async execute(interaction) {
try {
handleKickCommand(interaction)
} catch (error) {
console.error('There was an error in kicking:', error)
}
}
};

132
commands/music/bplay.js Normal file
View File

@ -0,0 +1,132 @@
const { Client, Interaction, SlashCommandBuilder, VoiceChannel } = require('discord.js');
const { playFromQueue, queue} = require('../../playback');
const ytdl = require('ytdl-core');
const { default: axios } = require('axios');
const { setStopped, cancelTimer } = require('../../playback')
var isDisabled = false
const API_KEY = process.env.YoutubeToken
/**
*
* @param {Client} client
* @param {Interaction} interaction
*/
async function addToQueue(interaction, link, songName) {
const voiceChannel = interaction.member.voice.channel;
const textChannel = interaction.channel;
const getMember = interaction.member.displayName;
const wasQueueEmpty = queue.length === 0;
queue.push({interaction, getMember, link, songName})
cancelTimer()
if (wasQueueEmpty) {
await playFromQueue(voiceChannel, textChannel, getMember, link, songName)
} else {
//console.log(queue)
interaction.reply(`Added to queue: ${songName}`)
}
}
async function searchYoutubeSong(input) {
try {
const response = await axios.get('https://www.googleapis.com/youtube/v3/search', {
params: {
part: 'snippet',
q: input,
key: API_KEY,
maxResults: 1,
type: 'video',
}
});
if (response.data.items && response.data.items.length > 0) {
const video = response.data.items[0];
const videoId = video.id.videoId;
const videoTitle = video.snippet.title;
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
return { videoUrl, videoTitle}
} else {
return null;
}
} catch (error) {
console.error('Error searching Youtube: ', error)
return null;
}
}
async function handlePlayCommand(interaction, input) {
const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) {
return interaction.reply({
content: 'You need to be in a voice channel to use this command.',
ephemeral: true
});
}
try {
let link = '';
let songName = input;
if (ytdl.validateURL(input)) {
// If it's a valid YouTube URL, extract the song information
const videoInfo = await ytdl.getBasicInfo(input);
songName = videoInfo.videoDetails.title;
link = input;
} else {
// If it's a search query, find the video
const searchResult = await searchYoutubeSong(input);
if (searchResult) {
link = searchResult.videoUrl;
songName = searchResult.videoTitle;
} else {
return interaction.reply({
content: 'No results found for your query.',
ephemeral: true,
});
}
}
// Add the song to the queue and play if needed
await addToQueue(interaction, link, songName);
} catch (error) {
console.error('Error handling play command:', error);
await interaction.reply({
content: 'An error occurred while processing your request.',
ephemeral: true,
});
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName('bplay')
.setDescription('Plays a song')
.addStringOption(option =>
option.setName('query')
.setDescription('The song name or URL to play')
.setRequired(true)
),
async execute(interaction) {
try {
if (isDisabled) return interaction.reply({ content: 'The command is Disabled... Try again later', ephemeral: true})
const query = interaction.options.getString('query');
setStopped(false)
await handlePlayCommand(interaction, query);
} catch (error) {
console.error('Error executing play command:', error);
await interaction.reply({
content: 'An error occurred while processing your request.',
ephemeral: true
});
}
}
};

36
commands/music/queue.js Normal file
View File

@ -0,0 +1,36 @@
const { Client, Interaction, SlashCommandBuilder, EmbedBuilder } = require('discord.js')
const { queue } = require('./../../playback')
const { musicQueueEmbed } = require('../../embeds/queue');
async function checkQueue(interaction) {
if (!queue || queue.length === 0 ) {
interaction.reply('The queue is empty.')
return
}
try {
const embed = musicQueueEmbed(queue);
await interaction.reply({ embeds: [embed] })
} catch (error) {
console.log('There is an error in the queue command: ', error)
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName('queue')
.setDescription('Check the playback queue'),
async execute(interaction) {
const voiceChannel = interaction.member.voice.channel
const textChannel = interaction.textChannel
if (!voiceChannel) {
return interaction.reply({
content: 'You need to be in a voice channel to use that queue',
ephemeral: true
})
}
await checkQueue(interaction, voiceChannel, textChannel)
}
}

60
commands/music/skip.js Normal file
View File

@ -0,0 +1,60 @@
const { Client, Interaction, SlashCommandBuilder } = require('discord.js')
const { playFromQueue, queue } = require('../../playback');
const { getVoiceConnection } = require('@discordjs/voice')
async function skipSong(voiceChannel, textChannel) {
const connection = getVoiceConnection(voiceChannel.guild.id)
if (!connection) {
console.log('No active voice connections')
textChannel.send('No active voice connections')
return
}
if (!queue || queue.length === 0) {
console.log('Queue is empty. No songs to skip')
connection.state.subscription.player.stop();
return;
}
try {
queue.shift()
if (queue.length > 0 ) {
const nextSong = queue[0];
await playFromQueue(voiceChannel, textChannel, nextSong.getMember, nextSong.link, nextSong.songName)
} else {
textChannel.send('No more songs in the queue')
}
} catch (error) {
console.error('Error skipping song: ', error)
}
}
module.exports = {
data: new SlashCommandBuilder()
.setName('skip')
.setDescription('Skips a song in the queue'),
async execute(interaction) {
const voiceChannel = interaction.member.voice.channel
const textChannel = interaction.channel
if (!voiceChannel) {
return interaction.reply({
content: 'You need to be in a voice channel to use the command',
ephemeral: true
});
}
try {
await skipSong(voiceChannel, textChannel)
interaction.reply('Skipped the current song')
} catch (error) {
console.error('Error in skip command: ', error)
interaction.reply({
content: 'An error has occurred while processing the command',
ephemeral: true
});
}
}
}

20
commands/music/stop.js Normal file
View File

@ -0,0 +1,20 @@
const { SlashCommandBuilder } = require("discord.js");
const { resetAll, setStopped } = require("../../playback");
module.exports = {
data: new SlashCommandBuilder()
.setName("stop")
.setDescription("Stop playback and leave the voice channel"),
async execute(interaction) {
const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) {
return interaction.reply({ content: "You need to be in a voice channel to use this command.", ephemeral: true });
}
setStopped(true); // Mark playback as stopped
resetAll(voiceChannel.guild.id); // Reset everything for a clean state
return interaction.reply("Stopped playback and left the voice channel.");
},
};

View File

@ -0,0 +1,46 @@
const {
Client,
Interaction,
ApplicationCommandOptionType,
PermissionFlagsBits,
SlashCommandBuilder,
} = require("discord.js");
const db = require("../../db");
module.exports = {
data: new SlashCommandBuilder()
.setName("autowelcome")
.setDescription("Enable or disable auto-welcome messages.")
.addChannelOption((option) =>
option
.setName("channel")
.setDescription(
"Channel Id to send welcome messages (required when enabling)"
)
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
const channelId = interaction.options.getChannel("channel");
const guildId = interaction.guild.id;
if (channelId) {
await db.query(
`INSERT INTO auto_welcome (guild_id, channel_id, enabled)
VALUES ($1, $2, $3)
ON CONFLICT (guild_id) DO UPDATE SET channel_id = $2, enabled = $3`,
[guildId, channelId.id, true]
);
return interaction.reply(
`Auto-welcome enabled! Messages will be sent to <#${channelId.id}>.`
);
} else {
await db.query(
`UPDATE auto_welcome SET enabled = $1 WHERE guild_id = $2`,
[false, guildId]
);
return interaction.reply("Auto-welcome has been disabled.");
}
},
};

17
commands/utility/ping.js Normal file
View File

@ -0,0 +1,17 @@
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with Pong! and websocket ping"),
async execute(interaction) {
// Send the initial reply and wait for it to be sent
const reply = await interaction.reply({
content: "Pong!",
fetchReply: true // This ensures we can get the reply object
});
const ping = reply.createdTimestamp - interaction.createdTimestamp; // Calculate the ping
await interaction.editReply(`Pong! client ${ping}ms | websocket: ${interaction.client.ws.ping}ms`);
},
};

View File

@ -0,0 +1,33 @@
const { Client, Interaction, ApplicationCommandOptionType, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");
const db = require("../../db");
module.exports = {
data: new SlashCommandBuilder()
.setName("setbluchannel")
.setDescription("Set the channel for Blu to send messages.")
.addChannelOption((option) =>
option
.setName("channel")
.setDescription("Channel to set for Blu messages")
.setRequired(true)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
const channelId = interaction.options.getChannel("channel");
const guildId = interaction.guild.id;
const guildName = interaction.guild.name;
const channelName = channelId.name;
if (channelId) {
await db.query(
`INSERT INTO bot_channel (guild_id, channel_id, guild_name, channel_name)
VALUES ($1, $2, $3, $4)
ON CONFLICT (guild_id) DO UPDATE SET Channel_id = $2, guild_name = $3, channel_name = $4`,
[guildId, channelId.id, guildName, channelName]
);
return interaction.reply(`Blu messages will be sent to <#${channelId.id}>.`);
} else {
return interaction.reply("Please provide a valid channel.");
}
},
}

37
db.js Normal file
View File

@ -0,0 +1,37 @@
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
host: process.env.PGHOST,
user: process.env.PGUSER,
database: process.env.PGDATABASE,
password: process.env.PGPASSWORD,
port: process.env.PGPORT,
});
async function connectDB() {
try {
await pool.connect();
console.log('Database connected.')
} catch (error) {
console.error('Failed to connect to the database: ',error)
process.exit(1)
}
}
function closeDB() {
try {
pool.end();
console.log('Database connection closed.')
} catch (error) {
console.error('Error closing the database: ', error )
}
}
module.exports = {
query: (text, params) => pool.query(text, params),
closeDB,
connectDB
};

View File

@ -0,0 +1,46 @@
const { REST, Routes } = require('discord.js');
require('dotenv').config();
const fs = require('node:fs');
const path = require('node:path');
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'));
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
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());
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
// Construct and prepare an instance of the REST module
const rest = new REST().setToken(process.env.TOKEN);
// and deploy your commands!
(async () => {
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.applicationGuildCommands(process.env.ClientID, process.env.GuildID),
{ 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);
}
})();

46
deploy-commands-public.js Normal file
View File

@ -0,0 +1,46 @@
const { REST, Routes } = require('discord.js');
require('dotenv').config();
const fs = require('node:fs');
const path = require('node:path');
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'));
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
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());
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
// Construct and prepare an instance of the REST module
const rest = new REST().setToken(process.env.TOKEN);
// and deploy your commands!
(async () => {
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(process.env.ClientID),
{ 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);
}
})();

44
embeds/modLogs.js Normal file
View File

@ -0,0 +1,44 @@
const { EmbedBuilder } = require('discord.js')
function modLogEmbed(action, member, reason ) {
let title, description, color;
//console.log(reason);
switch (action) {
case "ban":
title = ":hammer: User Banned";
description = `**User:** **${member.user.username}** has been banned.`;
reason2 = `**Reason:** ${reason}`;
color = "#FF0000";
break;
case "unban":
title = ":tada::party_popper: User Unbanned";
description = `**User:** **${member.user.username}** has been unbanned.`;
reason2 = `**Reason:** ${reason}`;
color = "#00FF00";
break;
case "mute":
return muteEmbed(member);
case "unmute":
return unmuteEmbed(member);
case "warn":
return warnEmbed(member);
default:
return null;
}
return new EmbedBuilder()
.setTitle(title)
.setDescription(`${description}\n${reason2}`)
.setColor(color)
.setTimestamp()
.setFooter({ text: `User ID: ${member.user.id}` })
.setThumbnail(member.user.displayAvatarURL({ dynamic: true }));
}
module.exports = { modLogEmbed };

19
embeds/nowPlaying.js Normal file
View File

@ -0,0 +1,19 @@
const { EmbedBuilder } = require('discord.js')
function nowPlayingEmbed(queue) {
if (!queue || queue.length === 0) {
return new EmbedBuilder()
.setTitle('**🎵 Now Playing:**')
.setDescription('No song is currently playing.')
.setColor('Grey');
}
const currentSong = queue[0]
return new EmbedBuilder()
.setTitle('**🎵 Now Playing:**')
.setDescription(`${currentSong.songName}\n**Requested By:** ${currentSong.getMember || 'Unknown User'}`)
.setColor('Green')
}
module.exports = { nowPlayingEmbed }

15
embeds/queue.js Normal file
View File

@ -0,0 +1,15 @@
const { EmbedBuilder } = require('discord.js')
function musicQueueEmbed(queue) {
const songLines = queue.map((item, index) =>
index === 0 ? `**🎵 Now Playing:** ${item.songName} **Requested By:** ${item.getMember}\n` : `${index + 1}. ${item.songName} - **Requested By:** ${item.getMember}\n`
);
return new EmbedBuilder()
.setTitle('Current Queue')
.setDescription(songLines.join('\n') || 'No Songs in the queue')
.setColor('Blue')
}
module.exports = { musicQueueEmbed }

33
embeds/userStreaming.js Normal file
View File

@ -0,0 +1,33 @@
const { EmbedBuilder, ButtonBuilder, ActionRowBuilder } = require('discord.js');
function userStreamingEmbed(user, status) {
const username = user?.username || 'Unknown User';
const avatarURL = user?.displayAvatarURL({ dynamic: true }) || null;
const streamTitle = status?.details || 'No title available';
const gameName = status?.name || 'No game available';
const streamURL = status?.url || 'No URL available';
const embed = new EmbedBuilder()
.setTitle('📡 Live Stream Alert!')
.setDescription(`**${username}** Just went Live on Twitch!`)
.addFields( { name: 'Stream Title', value: streamTitle, inline: false }, { name: 'Game / Category', value: gameName, inline: true } )
.setColor('Green')
.setTimestamp()
.setFooter({ text: 'User Streaming Status' })
.setThumbnail(avatarURL);
const streamButton = new ButtonBuilder()
.setLabel('🎥 Watch Stream')
.setURL(streamURL)
.setStyle(5);
const row = new ActionRowBuilder()
.addComponents(streamButton);
return { embeds: [embed], components: [row] };
}
module.exports = {
userStreamingEmbed
};

32
embeds/welcomeMember.js Normal file
View File

@ -0,0 +1,32 @@
const { GuildMember, EmbedBuilder } = require("discord.js");
/**
* @param {Client} client
* @param {GuildMember} member
*/
function welcomeEmbed(member) {
let welcomeMessages = [
`Welcome to the server ${member.user.username}. We hope you enjoy your stay.`,
`We hope you brought pizza ${member.user.username}.`,
`Yippe.... Welcome to the server ${member.user.username}.`,
`A wild ${member.user.username} has appeared.`,
];
const randomMessage =
welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)];
return new EmbedBuilder()
.setTitle("**New Member**")
.setDescription(randomMessage)
.addFields(
{ name: "Joined at", value: `${member.joinedAt}`, inline: true },
{
name: "User Created at",
value: `${member.user.createdAt}`,
inline: true,
}
)
.setThumbnail(member.user.avatarURL());
}
module.exports = { welcomeEmbed };

View File

@ -0,0 +1,26 @@
const { Events } = require('discord.js');
module.exports = {
name: Events.InteractionCreate,
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 });
}
}
},
};

View File

@ -0,0 +1,33 @@
const { Events, GuildMember } = require("discord.js");
const { modLogEmbed } = require("../../embeds/modLogs");
module.exports = {
name: Events.GuildBanAdd,
async execute(member) {
try {
const auditLogs = await member.guild.fetchAuditLogs({ type: 22 });
const banEntry = auditLogs.entries.first();
const modLogsChannel = member.guild.channels.cache.find(channel => channel.name === 'mod-logs');
if (!banEntry) return;
const { target, executor, reason } = banEntry;
if (!modLogsChannel) return;
// Call modLogEmbed and send the embed
const embed = modLogEmbed("ban", member, reason || "No reason provided");
if (embed) {
await modLogsChannel.send({ embeds: [embed] });
//console.log(`Ban logged: ${target.tag} banned by ${executor.tag}`);
}
} catch (error) {
console.error(error);
}
},
};

View File

@ -0,0 +1,48 @@
const { Events, GuildMember } = require("discord.js");
const db = require("../../db");
/**
* @param {Client} client
* @param {GuildMember} member
*/
module.exports = {
name: Events.GuildBanAdd,
async execute(member) {
const auditLogs = await member.guild.fetchAuditLogs({ type: 22 });
const banEntry = auditLogs.entries.first();
try {
if (banEntry) {
const { reason, executor, target, createdAt } = banEntry;
const guildID = member.guild.id;
const guildName = member.guild.name;
const userID = target.id;
const userName = target.username;
const userTag = target.tag;
const avatarURL = target.displayAvatarURL({ dynamic: true });
const banDate = createdAt.toISOString();
const banReason = reason || "No reason provided";
const banExecutor = executor.tag;
await db.query(
"INSERT INTO bans (guild_ID, guild_Name, user_ID, username, user_Tag, avatar_URL, ban_Date, ban_Reason, ban_Executor) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
[
guildID,
guildName,
userID,
userName,
userTag,
avatarURL,
banDate,
banReason,
banExecutor,
]
);
}
} catch (error) {
console.error("There was an error", error);
}
},
};

View File

@ -0,0 +1,33 @@
const { Events, GuildMember } = require("discord.js");
const { modLogEmbed } = require("../../embeds/modLogs");
module.exports = {
name: Events.GuildBanRemove,
async execute(member) {
try {
const auditLogs = await member.guild.fetchAuditLogs({ type: 23 });
const banEntry = auditLogs.entries.first();
const modLogsChannel = member.guild.channels.cache.find(channel => channel.name === 'mod-logs');
if (!banEntry) return;
const { target, executor, reason } = banEntry;
if (!modLogsChannel) return;
// Call modLogEmbed and send the embed
const embed = modLogEmbed("unban", member, reason || "No reason provided");
if (embed) {
await modLogsChannel.send({ embeds: [embed] });
//console.log(`Ban logged: ${target.tag} banned by ${executor.tag}`);
}
} catch (error) {
console.error(error);
}
},
};

View File

@ -0,0 +1,21 @@
const { Guild, Events } = require('discord.js');
const db = require('../../db');
module.exports = {
name: Events.GuildCreate,
async execute(guild) {
try {
await db.query('CREATE TABLE IF NOT EXISTS guilds (guild_id VARCHAR PRIMARY KEY, guild_name VARCHAR, created_at TIMESTAMP, guild_owner_id VARCHAR)');
const result = await db.query('SELECT * FROM guilds WHERE guild_id = $1', [guild.id]);
if (result.rows.length === 0) {
await db.query('INSERT INTO guilds (guild_id, guild_name, created_at, guild_owner_id) VALUES ($1, $2, $3, $4)', [guild.id, guild.name, guild.createdAt, guild.ownerId]);
console.log(`Added new guild to the database: ${guild.name}`);
} else {
console.log(`Guild already exists in the database: ${guild.name}`);
}
} catch (error) {
console.error('Error adding guild to the database:', error);
}
},
};

View File

@ -0,0 +1,34 @@
const { Guild, Events, PermissionFlagsBits } = require('discord.js');
const db = require('../../db');
module.exports = {
name: Events.GuildCreate,
async execute(guild) {
try {
await db.query('CREATE TABLE IF NOT EXISTS bot_channel (guild_id VARCHAR PRIMARY KEY, guild_name VARCHAR, channel_id VARCHAR, channel_name VARCHAR)');
const result = await db.query('SELECT * FROM bot_channel WHERE guild_id = $1', [guild.id]);
if (result.rows.length === 0) {
// Create Channel then add to database
const channel = await guild.channels.create({
name: 'BluBot',
type: 0,
permissionOverwrites: [
{
id: guild.id,
allow: [PermissionFlagsBits.ViewChannel],
},
],
});
await db.query('INSERT INTO bot_channel (guild_id, guild_name, channel_id, channel_name) VALUES ($1, $2, $3, $4)', [guild.id, guild.name, channel.id, channel.name]);
//await db.query('INSERT INTO bot_channel (guild_id, guild_name, channel_id, channel_name) VALUES ($1, $2, $3, $4)', [guild.id, guild.name, guild.channels.id, guild.channels.name]);
console.log(`Added new guild to the database: ${guild.name}`);
} else {
console.log(`Guild already exists in the database: ${guild.name}`);
}
}
catch (error) {
console.error('Error creating channel or adding to database:', error);
}
}
};

View File

@ -0,0 +1,26 @@
const { Events, GuildMember } = require("discord.js");
const db = require("../../db");
/**
* @param {Client} client
* @param {GuildMember} member
*/
module.exports = {
name: Events.GuildMemberAdd,
async execute(member) {
try {
const userID = member.id;
const userName = member.user.username;
const userTag = member.user.tag;
const avatarURL = member.displayAvatarURL({ dynamic: true });
await db.query(
"INSERT INTO members (user_ID, username, user_Tag, avatar_URL) VALUES ($1, $2, $3, $4) ON CONFLICT (user_ID) DO NOTHING",
[userID, userName, userTag, avatarURL]
);
} catch (error) {
console.error("There was an error", error);
}
},
};

View File

@ -0,0 +1,34 @@
const { GuildMember, Events } = require('discord.js')
const db = require('../../db')
const { welcomeEmbed } = require('../../embeds/welcomeMember')
/**
* @param {Client} client
* @param {GuildMember} member
*/
module.exports = {
name: Events.GuildMemberAdd,
async execute(member) {
try {
const embed = welcomeEmbed(member)
const result = await db.query(`SELECT channel_id, enabled FROM auto_welcome WHERE guild_id = $1`,
[member.guild.id])
const welcomeData = result.rows[0]
if (welcomeData && welcomeData.enabled) {
const welcomeChannel = member.guild.channels.cache.get(welcomeData.channel_id)
if (welcomeChannel) {
welcomeChannel.send({ embeds: [embed]})
} else {
console.error('Invalid channel id')
}
}
} catch (error) {
console.error('There was an error in sendWelcome:', error)
}
}
}

View File

@ -0,0 +1,26 @@
const { Events, Message } = require('discord.js');
const db = require('../../db')
/**
* @param { Message } message
*/
module.exports = {
name: Events.MessageCreate,
async execute(message) {
const messageContent = message.content
const author = message.author.tag
const guildID = message.guild.id
if (!message.inGuild() || message.author.bot) return;
try {
await db.query("INSERT INTO messages (message, author, guild_ID) VALUES ($1, $2, $3)", [
messageContent,
author,
guildID,
]);
} catch (error) {
console.error("Error saving message: ", error)
}
}
}

View File

@ -0,0 +1,47 @@
const { Guild, GuildMember, Events } = require('discord.js')
const db = require('../../db')
const { userStreamingEmbed } = require('../../embeds/userStreaming')
module.exports = {
name: Events.PresenceUpdate,
async execute(oldPresence, newPresence) {
//console.log('Presence Update:', newPresence);
// Check if the user is streaming
if (!newPresence.activities) return;
const streamingActivity = newPresence.activities.find(activity => activity.type === 1);
if (!streamingActivity) return;
const user = newPresence.user;
if (streamingActivity)
{
try {
for (const [guildId, guild] of newPresence.client.guilds.cache) {
const member = await guild.members.fetch(user.id).catch(() => null);
if (!member) continue;
const result = await db.query("SELECT channel_id FROM bot_channel WHERE guild_id = $1", [guildId]);
if (result.rows.length === 0) continue;
const channelId = result.rows[0].channel_id;
const channel = guild.channels.cache.get(channelId);
if (channel && channel.isTextBased()) {
const { embeds, components } = userStreamingEmbed(user, streamingActivity);
await channel.send({
embeds,
components
});
}
}
} catch (error) {
console.error('Error fetching user:', error);
return;
}
}
},
};

10
events/ready/ready.js Normal file
View File

@ -0,0 +1,10 @@
const { Events, PresenceUpdateStatus } = require('discord.js')
module.exports = {
name: Events.ClientReady,
once: true,
execute(client) {
console.log(`Ready! Logged in as ${client.user.tag}`);
client.user.setStatus(PresenceUpdateStatus.Online)
},
};

View File

@ -0,0 +1,41 @@
const { Client, Guild, Events, ActivityType, PresenceUpdateStatus } = require('discord.js');
/**
*
* @param {Client} client
* @param {Guild} guild
*/
module.exports = {
name: Events.ClientReady,
async execute(client) {
const guildsCount = client.guilds.cache.size;
let status = [
{
name: "I am Live",
type: ActivityType.Watching,
},
{
name: "Choo Choo 🚂",
},
{
name: 'Hippity Hoppity',
},
{
name: `I am in ${guildsCount} Server(s)`,
},
{
name: 'Yippe 🐻'
},
];
setInterval(() => {
let random = Math.floor(Math.random() * status.length);
client.user.setActivity(status[random]);
}, 30000);
}
};

View File

73
index.js Normal file
View File

@ -0,0 +1,73 @@
const fs = require('node:fs');
const path = require('node:path')
const {Client, Events, GatewayIntentBits, Collection, InteractionResponse } = require('discord.js');
require('dotenv').config();
const { connectDB, closeDB } = require('./db')
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences ] });
client.commands= new Collection();
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
for (const folder of commandFolders) {
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);
// Set a new item in the Collection with the key as the command name and the value as the exported module
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
function loadEvents(dir, client) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
// Recursively load events from subdirectories
loadEvents(fullPath, client);
} else if (file.isFile() && file.name.endsWith('.js')) {
// Load the event file
const event = require(fullPath);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
//console.log(`Loaded event: ${event.name}`);
}
}
}
// Call the function to load all events
const eventsPath = path.join(__dirname, 'events');
loadEvents(eventsPath, client);
connectDB().then(() => {
client.login(process.env.TOKEN).catch(console.error)
});
process.on('SIGINT', async () => {
console.log('Bot is shutting down...')
closeDB();
process.exit(0)
});
process.on('SIGTERM', async () => {
console.log('Bot received termination signal...');
closeDB();
process.exit(0)
})

1545
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "blubot",
"version": "1.1.0",
"main": "index.js",
"scripts": {
"run": "node .",
"dev": "nodemon ."
},
"author": "Megabyte",
"license": "ISC",
"description": "",
"dependencies": {
"@discordjs/voice": "^0.17.0",
"@distube/ytdl-core": "^4.14.4",
"axios": "^1.7.7",
"discord-api-types": "^0.37.119",
"discord.js": "^14.18.0",
"dotenv": "^16.4.5",
"ffmpeg": "^0.0.4",
"ffmpeg-static": "^5.2.0",
"libsodium-wrappers": "^0.7.15",
"nodemon": "^3.1.9",
"opusscript": "^0.0.8",
"pg": "^8.13.0",
"play-dl": "^1.9.7",
"undici": "^7.7.0",
"ytdl-core": "^4.11.5"
}
}

171
playback.js Normal file
View File

@ -0,0 +1,171 @@
const { Client, Interaction, Member } = require("discord.js");
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
NoSubscriberBehavior,
AudioPlayerStatus,
} = require("@discordjs/voice");
const ytdl = require("@distube/ytdl-core");
const { nowPlayingEmbed } = require("./embeds/nowPlaying");
const queue = [];
const activeChannels = {};
let stopped = false;
let timeout;
/**
*
* @param {Client} client
* @param {Interaction} interaction
*/
function resetAll(guildId) {
//console.log(`Resetting all: ${guildId}`)
queue.length = 0;
cancelTimer()
const connection = activeChannels[guildId]
if (connection) {
connection.destroy()
// console.log('Conection Destroyed')
}
delete activeChannels[guildId]
stopped = false
//console.log('All States reset')
}
function startTimer(connection) {
console.log(`Disconnect timer started `);
timeout = setTimeout(() => {
connection.destroy();
}, 30_000);
}
function cancelTimer() {
if (timeout) {
clearTimeout(timeout);
console.log("Disconnect timer stopped");
timeout = null;
}
}
async function playFromQueue(
voiceChannel,
textChannel,
getMemeber,
videoUrl,
videoTitle
) {
if (queue.length === 0) {
if (textChannel && activeChannels[voiceChannel.id] === textChannel.id) {
textChannel.send("Queue has finished playing.");
delete activeChannels[voiceChannel.id];
}
return;
}
//console.log("Current queue: ", queue)
const { interaction, link, songName } = queue[0];
try {
let connection = activeChannels[interaction.guild.id];
// Join the voice channel
if (!connection) {
connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: interaction.guild.id,
adapterCreator: interaction.guild.voiceAdapterCreator,
});
activeChannels[interaction.guild.id] = connection;
}
// Stream audio using ytdl-core
const stream = ytdl(videoUrl, {
filter: "audioonly",
highWaterMark: 1 << 25, // Larger buffer size
quality: "highestaudio",
requestOptions: {
headers: { Connection: "keep-alive" },
},
});
const resource = createAudioResource(stream, { inlineVolume: true });
resource.volume.setVolume(1); // Set volume to 1 (max)
const player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
player.play(resource);
connection.subscribe(player);
player.on(AudioPlayerStatus.Playing, () => {
if (startTimer) {
cancelTimer()
} else {
console.log('No timer running')
}
})
player.on(AudioPlayerStatus.Idle, () => {
//console.log('The audio player is idle.');
if (stopped) {
console.log("Playback was stopped by user");
return;
}
queue.shift();
if (queue.length > 0) {
const nextsong = queue[0];
playFromQueue(
voiceChannel,
textChannel,
nextsong.getMember,
nextsong.link,
nextsong.songName
);
} else {
textChannel.send("The queue is empty.");
if (connection) {
startTimer(connection);
} else {
return;
}
}
});
if (interaction.replied || interaction.deferred) {
const embed = nowPlayingEmbed(queue);
await interaction.followUp({ embeds: [embed] });
} else {
const embed = nowPlayingEmbed(queue);
await interaction.reply({ embeds: [embed] });
}
// Handle errors
player.on("error", (error) => {
console.error("Error with the audio player:", error);
interaction.followUp({
content: "There was an error playing the music.",
ephemeral: true,
});
});
} catch (error) {
console.error("Error in playback.js: ", error);
}
}
module.exports = {
playFromQueue,
queue,
cancelTimer,
setStopped: (value) => {
stopped = value;
},
getConnection: (guildId) => activeChannels[guildId], // Helper to get connection
resetAll,
};