Add features

This commit is contained in:
2024-09-12 14:37:08 +00:00
parent f5948621f0
commit a89150fc2f
22 changed files with 1145 additions and 99 deletions

99
src/commands/ai/image.js Normal file
View File

@@ -0,0 +1,99 @@
import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js';
import axios from 'axios';
import Replicate from 'replicate';
if (!process.env.REPLICATE_API_KEY) {
throw new Error('REPLICATE_API_KEY is not set in the environment variables.');
}
const replicate = new Replicate({
auth: process.env.REPLICATE_API_KEY,
});
const data = new SlashCommandBuilder()
.setName('image')
.setDescription('Generate an image based on a prompt.')
.addStringOption((option) =>
option
.setName('prompt')
.setDescription('The prompt to generate an image from')
.setRequired(true)
);
// Helper function to poll the prediction status
async function pollPredictionStatus(
predictionId,
maxAttempts = 5,
interval = 2000
) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const latestPrediction = await replicate.predictions.get(predictionId);
if (
latestPrediction.status !== 'starting' &&
latestPrediction.status !== 'processing'
) {
return latestPrediction;
}
// Wait before checking again
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Prediction timed out.');
}
// Helper function to download the image from a URL
async function downloadImage(url) {
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
return Buffer.from(response.data);
} catch (error) {
throw new Error('Failed to download the image.');
}
}
async function execute(interaction) {
await interaction.deferReply();
if (!process.env.REPLICATE_API_KEY) {
await interaction.reply('The bot is missing the Replicate API key.');
return;
}
const prompt = interaction.options.getString('prompt');
try {
// Create image generation prediction
const prediction = await replicate.predictions.create({
model: 'black-forest-labs/flux-schnell',
input: { prompt },
});
// Poll until the image generation is complete
const completedPrediction = await pollPredictionStatus(prediction.id);
if (!completedPrediction || !completedPrediction.output) {
throw new Error('Failed to generate the image.');
}
const imageUrl = completedPrediction.output[0];
// Download the generated image
const imageBuffer = await downloadImage(imageUrl);
// Create an attachment to send the image back to the user
const attachment = new AttachmentBuilder(imageBuffer, {
name: 'image.png',
});
// Edit the deferred reply to include the generated image
await interaction.editReply({ files: [attachment] });
} catch (error) {
// Provide a more informative error message to the user
console.error(error); // Log the error for debugging purposes
await interaction.editReply(`An error occurred: ${error.message}`);
}
}
export default { data, execute };

207
src/commands/db/game.js Normal file
View File

@@ -0,0 +1,207 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
import {
listGameNames,
getGame,
setGame,
deleteGame,
deleteField,
} from '../../utils/db/game.js';
const data = new SlashCommandBuilder()
.setName('game')
.setDescription('Perform ButlerBot game database operations.')
.addSubcommand((subcommand) =>
subcommand.setName('list').setDescription('List all games in the database.')
)
.addSubcommand((subcommand) =>
subcommand
.setName('get')
.setDescription('Get a game from the database.')
.addStringOption((option) =>
option
.setName('game')
.setDescription('The name of the game to get.')
.setRequired(true)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('set')
.setDescription('Set a key value pair on an game in the database.')
.addStringOption((option) =>
option
.setName('game')
.setDescription('The name of the game to set the field in.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('key')
.setDescription('The key or label to set.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('value')
.setDescription('The value of the field to set.')
.setRequired(true)
.setMinLength(1)
.setMaxLength(1024)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('delete')
.setDescription('Delete a game, or a field from a game.')
.addStringOption((option) =>
option
.setName('game')
.setDescription(
'The name of the game to remove, or remove a field from.'
)
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('key')
.setDescription('The key or label of the field to remove.')
.setRequired(false)
)
);
function createEmbedFromGame(game) {
const embed = new EmbedBuilder().setTitle(game.game).setColor(0xff0000);
// Add fields for each key value pair - skip name, _id and guild.
embed.addFields(
Object.entries(game)
.filter(([key]) => !['_id', 'game', 'guild'].includes(key))
.map(([key, value]) => ({ name: key, value: String(value) }))
);
if (embed.length > 6000) {
throw new Error('Embed size exceeds maximum.');
}
return embed;
}
async function execute(interaction) {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case 'list': {
const gameNames = await listGameNames();
if (gameNames.length === 0) {
await interaction.editReply('No games found in the database.');
return;
}
const embed = new EmbedBuilder()
.setTitle('Game Database')
.setColor(0xff0000)
.setDescription('List of games in the database.');
embed.addFields({
name: 'Games',
value: gameNames.map((game) => game.game).join('\n'),
});
await interaction.editReply({ embeds: [embed] });
break;
}
case 'get': {
const gameName = interaction.options.getString('game');
const game = await getGame(gameName);
if (!game) {
await interaction.editReply(
`Game ${gameName} not found in the database.`
);
return;
}
try {
const embed = createEmbedFromGame(game);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(
'Embed too large to send. Game has not been retrieved.'
);
}
break;
}
case 'set': {
const gameName = interaction.options.getString('game');
const key = interaction.options.getString('key');
const value = interaction.options.getString('value');
const oldGame = await getGame(gameName);
await setGame(gameName, key, value);
const updatedGame = await getGame(gameName);
try {
const embed = createEmbedFromGame(updatedGame);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
// Revert game back to original state if the embed is too large.
if (oldGame) {
// If key already existed, revert to old value, else delete the key.
if (oldGame[key]) {
await setGame(gameName, key, oldGame[key]);
} else {
await deleteField(gameName, key);
}
}
await interaction.editReply(
'Embed too large to send. Game has not been updated.'
);
}
break;
}
case 'delete': {
const gameName = interaction.options.getString('game');
const key = interaction.options.getString('key');
if (key) {
await deleteField(gameName, key);
const updatedGame = await getGame(gameName);
if (!updatedGame) {
await interaction.editReply(
`Game ${gameName} deleted from the database.`
);
return;
}
try {
const embed = createEmbedFromGame(updatedGame);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(
'Embed too large to send. Field deleted from game but game has not been retrieved.'
);
}
} else {
await deleteGame(gameName);
await interaction.editReply(
`Game ${gameName} deleted from the database.`
);
}
break;
}
default:
await interaction.editReply('Unknown subcommand.');
break;
}
}
export default { data, execute };

View File

@@ -1,6 +1,10 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import axios from 'axios';
if (!process.env.OMDB_API_KEY) {
throw new Error('OMDB_API_KEY is not set in the environment variables.');
}
const OMDB_API_URL = 'http://www.omdbapi.com/?apikey={apiKey}';
const FIELDS = [
'Title',
@@ -32,40 +36,65 @@ const data = new SlashCommandBuilder()
.setMaxValue(2100)
);
async function execute(interaction) {
const filmName = interaction.options.getString('film_name');
const filmYear = interaction.options.getInteger('film_year');
let embed = new EmbedBuilder().setTitle('IMDB').setColor(0xf5de50);
let omdbQueryUrl = OMDB_API_URL.replace('{apiKey}', process.env.OMDB_API_KEY);
omdbQueryUrl += `&t=${encodeURIComponent(filmName.toLowerCase())}`;
// Helper function to construct OMDB query URL
function buildOmdbUrl(filmName, filmYear) {
let url = OMDB_API_URL.replace('{apiKey}', process.env.OMDB_API_KEY);
url += `&t=${encodeURIComponent(filmName.toLowerCase())}`;
if (filmYear) {
omdbQueryUrl += `&y=${filmYear}`;
url += `&y=${filmYear}`;
}
return url;
}
try {
const response = await axios.get(omdbQueryUrl, { timeout: 5000 });
const result = response.data;
async function execute(interaction) {
await interaction.deferReply();
FIELDS.forEach((field) => {
const fieldValue = result[field];
if (fieldValue) {
embed.addFields({ name: field, value: fieldValue });
}
});
const poster = result.Poster;
if (poster) {
embed.setImage(poster);
}
} catch (error) {
console.error(`Error looking up film: ${filmName}.`, error);
await interaction.reply('An error occurred when querying the OMDB API.');
if (!process.env.OMDB_API_KEY) {
await interaction.editReply('The bot is missing the OMDB API key.');
return;
}
await interaction.reply({ embeds: [embed] });
const filmName = interaction.options.getString('film_name');
const filmYear = interaction.options.getInteger('film_year');
try {
const omdbQueryUrl = buildOmdbUrl(filmName, filmYear);
const response = await axios.get(omdbQueryUrl, { timeout: 5000 });
const result = response.data;
// Check if the movie was found
if (result.Response === 'False') {
await interaction.editReply(
`No results found for the film: ${filmName}.`
);
return;
}
// Build the embed with film details
const embed = new EmbedBuilder()
.setTitle(result.Title || 'IMDB Film Info')
.setColor(0xf5de50);
// Add the fields to the embed
FIELDS.forEach((field) => {
if (result[field]) {
embed.addFields({ name: field, value: result[field], inline: true });
}
});
// Add poster image if available
if (result.Poster && result.Poster !== 'N/A') {
embed.setImage(result.Poster);
}
// Send the result to the user
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error(`Error looking up film: ${filmName}`, error);
await interaction.editReply(
'An error occurred when querying the OMDB API.'
);
}
}
export default { data, execute };

View File

@@ -5,6 +5,10 @@ const API_URL = 'https://my-api.plantnet.org/v2/identify/all?api-key={apiKey}';
const LEAF_THUMBNAIL =
'https://cdn.discordapp.com/attachments/870024275556446328/1006249009201033287/monstera.png';
if (!process.env.PLANTNET_API_KEY) {
throw new Error('PLANTNET_API_KEY is not set in the environment variables.');
}
const data = new SlashCommandBuilder()
.setName('plant')
.setDescription('Identify a plant by uploading an image.')
@@ -15,14 +19,48 @@ const data = new SlashCommandBuilder()
.setRequired(true)
);
// Helper function to generate plant details
function generatePlantDetail(commonName, scientificName, confidence) {
let detail = '';
if (0.0 <= confidence && confidence < 0.25) {
detail = `I'm really not sure, but I think that looks like a **${commonName}**?`;
} else if (0.25 <= confidence && confidence < 0.5) {
detail = `I think that looks like a **${commonName}**!`;
} else if (0.5 <= confidence && confidence < 0.75) {
detail = `That looks like a **${commonName}**!`;
} else if (confidence >= 0.75) {
detail = `That is a really cool **${commonName}**!`;
}
const imageUrl = `https://www.google.com/search?q=${encodeURIComponent(
commonName || scientificName
)}&tbm=isch`;
detail += `\n\n**Scientific Name**: ${scientificName}`;
detail += `\n\n**Images**: [More images](${imageUrl})`;
detail += `\n\n**Confidence**: ${(confidence * 100).toFixed(3)}%`;
return detail;
}
async function execute(interaction) {
await interaction.deferReply();
if (!process.env.PLANTNET_API_KEY) {
await interaction.editReply('The bot is missing the PlantNet API key.');
return;
}
const image = interaction.options.getAttachment('image');
let embed = new EmbedBuilder()
.setTitle('Plant Detector™')
.setThumbnail(LEAF_THUMBNAIL)
.setColor(0xff0000);
try {
const embed = new EmbedBuilder()
.setTitle('Plant Detector™')
.setThumbnail(LEAF_THUMBNAIL)
.setColor(0xff0000);
// API call to identify the plant
const response = await axios.get(
API_URL.replace('{apiKey}', process.env.PLANTNET_API_KEY),
{
@@ -35,41 +73,38 @@ async function execute(interaction) {
);
const bestMatch = response.data.results[0];
// Check if there is a valid result
if (!bestMatch) {
await interaction.editReply(
'No plant could be identified from the image.'
);
return;
}
const commonName =
bestMatch.species.commonNames[0] || 'No common name found';
const scientificName = bestMatch.species.scientificNameWithoutAuthor;
const confidence = parseFloat(bestMatch.score);
let detail = '';
if (0.0 <= confidence && confidence < 0.25) {
detail = `I'm really not sure, but I think that looks like a **${commonName}**?`;
} else if (0.25 <= confidence && confidence < 0.5) {
detail = `I think that looks like a **${commonName}**!`;
} else if (0.5 <= confidence && confidence < 0.75) {
detail = `That looks like a **${commonName}**!`;
} else if (confidence >= 0.75) {
detail = `That is a really cool **${commonName}**!`;
}
const imageUrl = `https://www.google.com/search?q=${encodeURIComponent(commonName || scientificName)}&tbm=isch`;
detail += `\n\n**Scientific Name**: ${scientificName}`;
detail += `\n\n**Images**: [More images](${imageUrl})`;
detail += `\n\n**Confidence**: ${(confidence * 100).toFixed(3)}%`;
const plantDetails = generatePlantDetail(
commonName,
scientificName,
confidence
);
embed
.setColor(0x00ff00)
.addFields({ name: 'Plant Details', value: detail });
.addFields({ name: 'Plant Details', value: plantDetails });
// Send the result to the user
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error(`Error looking up plant: ${image.url}`, error);
await interaction.reply(
console.error(error); // Log the error for debugging
await interaction.editReply(
'An error occurred when querying the PlantNet API.'
);
return;
}
await interaction.reply({ embeds: [embed] });
}
export default { data, execute };

View File

@@ -7,33 +7,42 @@ const data = new SlashCommandBuilder()
.setName('eyecandy')
.setDescription('Returns a random gif of Gerard Butler.');
async function execute(interaction) {
const giphyApiKey = process.env.GIPHY_API_KEY;
if (!giphyApiKey) {
await interaction.reply(
'The bot has not been configured with a Giphy API key.'
);
return;
}
async function fetchGif() {
const randomOffset = Math.floor(Math.random() * 100);
const giphyQueryUrl = `${GIPHY_API_URL}?api_key=${giphyApiKey}&q=gerard+butler&limit=1&offset=${randomOffset}`;
const giphyQueryUrl = `${GIPHY_API_URL}?api_key=${process.env.GIPHY_API_KEY}&q=gerard+butler&limit=1&offset=${randomOffset}`;
try {
const response = await axios.get(giphyQueryUrl, { timeout: 5000 });
const result = response.data;
if (result.data.length === 0) {
await interaction.reply('No gifs found.');
return;
throw new Error('No gifs found for the given query.');
}
const imageUrl = result.data[0].images.original.url;
await interaction.reply(imageUrl);
return result.data[0].images.original.url;
} catch (error) {
console.error('Error querying the Giphy API:', error);
await interaction.reply('An error occurred when querying the Giphy API.');
console.error('Error fetching GIF:', error.message);
throw new Error('Failed to retrieve a GIF from Giphy.');
}
}
async function execute(interaction) {
await interaction.deferReply();
if (!process.env.GIPHY_API_KEY) {
await interaction.reply('The bot is missing the GIPHY API key.');
return;
}
try {
// Fetch a random gif of Gerard Butler.
const gifUrl = await fetchGif();
// Reply with the gif.
await interaction.editReply(gifUrl);
} catch (error) {
console.error('Error executing the command:', error.message);
await interaction.editReply(`An error occurred: ${error.message}`);
}
}

View File

@@ -5,19 +5,15 @@ const data = new SlashCommandBuilder()
.setDescription('Start a five second countdown.');
async function execute(interaction) {
// Initial response to acknowledge the command
await interaction.reply({ content: 'Starting countdown...' });
// Initial delay before starting the countdown
await new Promise((resolve) => setTimeout(resolve, 2000)); // 2-second delay
// Edit the response with the countdown
for (let i = 5; i > 0; i--) {
await interaction.editReply({ content: String(i) });
await new Promise((resolve) => setTimeout(resolve, 1000)); // 1-second delay between numbers
}
// Final message
await interaction.editReply({ content: '🎉 GO! 🎉' });
}

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
import { loadCommandModules } from './utilities/commandModules.js';
@@ -42,4 +43,4 @@ client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);
client.login(process.env.DISCORD_API_KEY);

132
src/music/music.js Normal file
View File

@@ -0,0 +1,132 @@
import { SlashCommandBuilder } from 'discord.js';
import { Interaction } from 'discord.js';
// Maps to track queues and voice connections per guild
const queueMap = new Map(); // { guildId: [track1, track2, ...] }
const connectionMap = new Map(); // { guildId: VoiceConnection }
const data = new SlashCommandBuilder()
.setName('music')
.setDescription('Perform ButlerBot music operations.')
.addSubcommand((subcommand) =>
subcommand
.setName('play')
.setDescription('Play a track.')
.addStringOption((option) =>
option
.setName('track')
.setDescription('The track to play.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('provider')
.setDescription('The provider of the track.')
.setRequired(false)
.addChoices(
{ name: 'YouTube', value: 'youtube' },
{ name: 'Spotify', value: 'spotify' },
{ name: 'Local', value: 'local' }
)
)
)
.addSubcommand((subcommand) =>
subcommand.setName('pause').setDescription('Pause the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('resume').setDescription('Resume the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('skip').setDescription('Skip the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('stop').setDescription('Stop the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('queue').setDescription('Show the current track queue.')
)
.addSubcommand((subcommand) =>
subcommand.setName('clear').setDescription('Clear the current track queue.')
)
.addSubcommand((subcommand) =>
subcommand.setName('leave').setDescription('Leave the voice channel.')
)
.addSubcommand((subcommand) =>
subcommand.setName('join').setDescription('Join the voice channel.')
);
/**
* Handles the music command.
* @param {Interaction} interaction The interaction object.
*/
async function execute(interaction) {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
const guildId = interaction.guild.id;
switch (subcommand) {
case 'play':
await handlePlay(interaction, guildId);
break;
case 'pause':
await handlePause(interaction, guildId);
break;
case 'resume':
await handleResume(interaction, guildId);
break;
case 'skip':
await handleSkip(interaction, guildId);
break;
case 'stop':
await handleStop(interaction, guildId);
break;
case 'queue':
await handleQueue(interaction, guildId);
break;
case 'clear':
await handleClear(interaction, guildId);
break;
case 'leave':
await handleLeave(interaction, guildId);
break;
case 'join':
await handleJoin(interaction, guildId);
break;
default:
await interaction.editReply('Invalid music subcommand.');
}
}
/**
* Handles the play subcommand.
* @param {Interaction} interaction The interaction object.
* @param {string} guildId The guild ID.
* @returns {Promise<void>}
*/
async function handlePlay(interaction, guildId) {
const track = interaction.options.getString('track');
const provider = interaction.options.getString('provider') || 'youtube';
// Check if the user is in a voice channel
const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) {
await interaction.editReply(
'You need to be in a voice channel to play music.'
);
return;
}
// Get the queue for the guild
const queue = queueMap.get(guildId) || [];
queue.push({ track, provider });
queueMap.set(guildId, queue);
// Join the voice channel
await handleJoin(interaction, guildId);
// Play the track
await interaction.editReply(`Playing track: ${track}`);
}
export default { data, execute };

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import { REST, Routes } from 'discord.js';
import { loadCommandModules } from './utilities/commandModules.js';
@@ -7,7 +8,7 @@ const commands = commandModules.map((commandModule) =>
commandModule.data.toJSON()
);
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
const rest = new REST().setToken(process.env.DISCORD_API_KEY);
try {
console.log('Started refreshing application (/) commands.');
@@ -19,6 +20,8 @@ try {
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
// Exit the process.
process.exit();
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);

37
src/utils/db/game.js Normal file
View File

@@ -0,0 +1,37 @@
import { MongoClient } from 'mongodb';
if (!process.env.MONGODB_URI) {
throw new Error('MONGODB_URI is not set in the environment variables.');
}
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const database = client.db('butler_db');
const collection = database.collection('GameFeatures');
export async function listGameNames() {
return collection.find({}, { projection: { game: 1 } }).toArray();
}
export async function getGame(game) {
return collection.findOne({ game });
}
export async function setGame(game, key, value) {
// If game is not found, create a new document with game field set to game, and key field set to value.
// Overwrite the value of the key field if it already exists.
return collection.updateOne(
{ game },
{ $set: { game, [key]: value } },
{ upsert: true }
);
}
export async function deleteGame(game) {
return collection.deleteOne({ game });
}
export async function deleteField(game, key) {
return collection.updateOne({ game }, { $unset: { [key]: '' } });
}