discordbot/index.js
ale f1e792d050
out console log
Signed-off-by: ale <ale@manalejandro.com>
2025-06-08 04:41:26 +02:00

884 lines
37 KiB
JavaScript

import { config } from 'dotenv';
import { Client, Events, GatewayIntentBits, SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } from 'discord.js';
import fetch from 'node-fetch';
import * as cheerio from 'cheerio';
import { createWriteStream, unlinkSync, existsSync } from 'fs';
import { pipeline } from 'stream/promises';
import path from 'path';
import { createRequire } from 'module';
// Create require function for CommonJS modules
const require = createRequire(import.meta.url);
const Socket = require('blockchain.info/Socket');
const blockexplorer = require('blockchain.info/blockexplorer');
// Load environment variables
config();
// Bitcoin monitoring variables
let btcMonitorChannel = null;
let btcSocket = null;
let isMonitoring = false;
let minBtcAmount = 0; // Minimum BTC amount to show transactions
let transactionHandler = null; // Store the transaction handler reference
let processedTransactions = new Set(); // Track processed transaction hashes
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages
]
});
client.on(Events.ClientReady, async readyClient => {
console.log(`Logged in as ${readyClient.user.tag}!`);
readyClient.user.setPresence({
activities: [{ name: 'Monitoring Bitcoin & Searching Media' }],
status: 'online',
});
try {
// Register all slash commands
const commands = [
{
name: 'ping',
description: 'Replies with Pong!'
},
{
name: 'send',
description: 'Send a message to a channel',
options: [
{
name: 'channel',
type: 7, // CHANNEL type
description: 'The channel to send the message to',
required: true
},
{
name: 'message',
type: 3, // STRING type
description: 'The message to send',
required: true
}
]
},
{
name: 'search',
description: 'Search for images or videos on Google',
options: [
{
name: 'query',
type: 3, // STRING type
description: 'What to search for',
required: true
},
{
name: 'type',
type: 3, // STRING type
description: 'Type of media to search for (image, video)',
required: true,
choices: [
{ name: 'Images', value: 'image' },
{ name: 'Videos', value: 'video' }
]
},
{
name: 'count',
type: 4, // INTEGER type
description: 'Number of results to show (1-5)',
required: false,
min_value: 1,
max_value: 5
}
]
},
{
name: 'download',
description: 'Download and display multimedia files',
options: [
{
name: 'url',
type: 3, // STRING type
description: 'Direct URL to the multimedia file',
required: true
},
{
name: 'title',
type: 3, // STRING type
description: 'Optional title for the file',
required: false
}
]
},
{
name: 'btc-monitor',
description: 'Start/stop Bitcoin transaction monitoring',
options: [
{
name: 'action',
type: 3, // STRING type
description: 'Action to perform',
required: true,
choices: [
{ name: 'Start', value: 'start' },
{ name: 'Stop', value: 'stop' },
{ name: 'Status', value: 'status' }
]
},
{
name: 'channel',
type: 7, // CHANNEL type
description: 'Channel to send Bitcoin updates (required for start)',
required: false
},
{
name: 'min-amount',
type: 10, // NUMBER type
description: 'Minimum BTC amount to show transactions (default: 0)',
required: false,
min_value: 0
}
]
}
];
console.log('Started refreshing application (/) commands.');
// Register each command
for (const command of commands) {
await readyClient.application.commands.create(command);
console.log(`Command /${command.name} registered successfully!`);
}
console.log('Successfully registered all application commands.');
} catch (error) {
console.error('Error registering slash commands:', error);
}
});
// Utility functions for multimedia handling
async function downloadFile(url, filePath) {
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
await pipeline(response.body, createWriteStream(filePath));
}
function getFileExtension(url) {
const urlPath = new URL(url).pathname;
return path.extname(urlPath).toLowerCase();
}
function isImageFile(url) {
const extension = getFileExtension(url);
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'].includes(extension);
}
function isVideoFile(url) {
const extension = getFileExtension(url);
return ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.flv'].includes(extension);
}
function isAudioFile(url) {
const extension = getFileExtension(url);
return ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'].includes(extension);
}
async function validateMultimediaUrl(url) {
try {
const response = await fetch(url, {
method: 'HEAD',
timeout: 5000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
const contentType = response.headers.get('content-type') || '';
const isMultimedia = contentType.startsWith('image/') ||
contentType.startsWith('video/') ||
contentType.startsWith('audio/');
return {
valid: response.ok && isMultimedia,
contentType,
size: response.headers.get('content-length')
};
} catch (error) {
return { valid: false, error: error.message };
}
}
// More lenient validation for search results
async function validateImageUrl(url) {
try {
// First check if the URL looks like an image file
const urlLower = url.toLowerCase();
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
const hasImageExtension = imageExtensions.some(ext => urlLower.includes(ext));
// If it has an image extension or is from known image hosting sites, consider it valid
const knownImageHosts = ['imgur.com', 'i.imgur.com', 'cdn.', 'images.', 'img.', 'static.', 'upload.wikimedia.org', 'picsum.photos', 'cataas.com', 'dog.ceo', 'images.dog.ceo', 'placeholder.com', 'dummyimage.com'];
const isFromImageHost = knownImageHosts.some(host => urlLower.includes(host));
if (hasImageExtension || isFromImageHost) {
return { valid: true, contentType: 'image/jpeg' };
}
// For other URLs, try a quick GET request to follow redirects
const response = await fetch(url, {
method: 'GET',
timeout: 3000,
redirect: 'follow',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
const contentType = response.headers.get('content-type') || '';
return {
valid: response.ok && (contentType.startsWith('image/') || contentType === ''),
contentType: contentType || 'image/jpeg'
};
} catch (error) {
// If validation fails, assume it might be valid anyway for search results
return { valid: true, contentType: 'image/jpeg', error: error.message };
}
}
// Real search function using DuckDuckGo API for actual search results
async function searchImages(query, limit = 10) {
try {
// Use DuckDuckGo Instant Answer API for real search results
const searchUrl = `https://api.duckduckgo.com/?q=${encodeURIComponent(query + ' image')}&format=json&no_html=1&skip_disambig=1&safe_search=strict`;
const response = await fetch(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
throw new Error(`Search API failed: ${response.status}`);
}
const data = await response.json();
const searchResults = [];
// Extract image results from DuckDuckGo response
if (data.Results && data.Results.length > 0) {
for (let i = 0; i < Math.min(data.Results.length, limit); i++) {
const result = data.Results[i];
if (result.Icon && result.Icon.URL) {
searchResults.push({
title: result.Text || `${query} - Result ${i + 1}`,
link: result.Icon.URL
});
}
}
}
// If no Results, try using Related Topics
if (searchResults.length === 0 && data.RelatedTopics && data.RelatedTopics.length > 0) {
for (let i = 0; i < Math.min(data.RelatedTopics.length, limit); i++) {
const topic = data.RelatedTopics[i];
if (topic.Icon && topic.Icon.URL) {
searchResults.push({
title: topic.Text || `${query} - Topic ${i + 1}`,
link: topic.Icon.URL
});
}
}
}
// If still no results, try scraping Unsplash for real photos
if (searchResults.length === 0) {
try {
const unsplashUrl = `https://unsplash.com/s/photos/${encodeURIComponent(query)}`;
const pageResponse = await fetch(unsplashUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (pageResponse.ok) {
const html = await pageResponse.text();
const $ = cheerio.load(html);
// Extract image URLs from Unsplash
$('img[src*="unsplash"]').each((i, element) => {
if (searchResults.length >= limit) return false;
const src = $(element).attr('src');
const alt = $(element).attr('alt') || `${query} - Image ${i + 1}`;
if (src && src.includes('unsplash')) {
searchResults.push({
title: alt,
link: src
});
}
});
}
} catch (unsplashError) {
// Unsplash scraping failed silently
}
}
return searchResults.slice(0, limit);
} catch (error) {
console.error('Search failed:', error);
throw new Error(`Failed to search for "${query}": ${error.message}`);
}
}
// Real video search function using YouTube search
async function searchVideos(query, limit = 5) {
try {
// Use DuckDuckGo to search for YouTube videos
const searchUrl = `https://api.duckduckgo.com/?q=${encodeURIComponent(query + ' site:youtube.com')}&format=json&no_html=1&skip_disambig=1`;
const response = await fetch(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
throw new Error(`Video search API failed: ${response.status}`);
}
const data = await response.json();
const videoResults = [];
// Extract video results from DuckDuckGo response
if (data.Results && data.Results.length > 0) {
for (let i = 0; i < Math.min(data.Results.length, limit); i++) {
const result = data.Results[i];
if (result.FirstURL && result.FirstURL.includes('youtube.com')) {
videoResults.push({
title: result.Text || `${query} - Video ${i + 1}`,
link: result.FirstURL,
snippet: result.Text || `Video related to: ${query}`
});
}
}
}
// Try scraping YouTube search results if DuckDuckGo doesn't return enough
if (videoResults.length === 0) {
try {
const youtubeSearchUrl = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
const pageResponse = await fetch(youtubeSearchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (pageResponse.ok) {
const html = await pageResponse.text();
// Extract video IDs from YouTube search results
const videoIdRegex = /"videoId":"([^"]+)"/g;
const titleRegex = /"title":{"runs":\[{"text":"([^"]+)"/g;
let match;
const videoIds = [];
const titles = [];
while ((match = videoIdRegex.exec(html)) !== null && videoIds.length < limit) {
videoIds.push(match[1]);
}
while ((match = titleRegex.exec(html)) !== null && titles.length < limit) {
titles.push(match[1]);
}
for (let i = 0; i < Math.min(videoIds.length, limit); i++) {
videoResults.push({
title: titles[i] || `${query} - Video ${i + 1}`,
link: `https://www.youtube.com/watch?v=${videoIds[i]}`,
snippet: `Video about: ${query}`
});
}
}
} catch (youtubeError) {
// YouTube scraping failed silently
}
}
return videoResults.slice(0, limit);
} catch (error) {
console.error('Video search failed:', error);
throw new Error(`Failed to search videos for "${query}": ${error.message}`);
}
}
// Bitcoin transaction monitoring functions
async function initBitcoinMonitoring() {
try {
// Make sure to stop any existing monitoring first
if (btcSocket || isMonitoring) {
stopBitcoinMonitoring();
// Wait a moment to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Clear processed transactions to start fresh
processedTransactions.clear();
// Create a completely new Socket instance
btcSocket = new Socket();
// Create the transaction handler function
transactionHandler = async (tx) => {
// Double check that monitoring is still active
if (!btcMonitorChannel || !isMonitoring || !btcSocket) return;
try {
// Check if we've already processed this transaction
if (processedTransactions.has(tx.hash)) {
return; // Skip silently
}
// Add to processed transactions
processedTransactions.add(tx.hash);
// Process transaction data
const txData = await processBitcoinTransaction(tx, blockexplorer);
// Check if transaction meets minimum amount threshold
if (parseFloat(txData.amount) < minBtcAmount) {
return; // Skip this transaction if it's below the minimum amount
}
// Create Discord embed
const embed = new EmbedBuilder()
.setTitle('🟠 New Bitcoin Transaction')
.setColor(0xF7931A)
.addFields(
{ name: '🔗 Transaction Hash', value: `\`${txData.hash}\``, inline: false },
{ name: '💰 Amount', value: `${txData.amount} BTC`, inline: true },
{ name: '📊 Size', value: `${txData.size} bytes`, inline: true },
{ name: '💸 Fee', value: txData.fee !== 'Unknown' ? `${(txData.fee / 100000000).toFixed(8)} BTC` : 'Unknown', inline: true },
{ name: '⚡ Fee Rate', value: `${txData.feeRate} sat/byte`, inline: true }
)
.setTimestamp()
.setFooter({ text: `Live Bitcoin Network | Min: ${minBtcAmount} BTC` });
// Add input addresses (max 5)
if (txData.inputAddresses.length > 0) {
const inputs = txData.inputAddresses.slice(0, 5).join('\n');
embed.addFields({
name: `📤 Inputs (${txData.inputAddresses.length})`,
value: `\`\`\`${inputs}${txData.inputAddresses.length > 5 ? '\n...' : ''}\`\`\``,
inline: false
});
}
// Add output addresses (max 5)
if (txData.outputAddresses.length > 0) {
const outputs = txData.outputAddresses.slice(0, 5).join('\n');
embed.addFields({
name: `📥 Outputs (${txData.outputAddresses.length})`,
value: `\`\`\`${outputs}${txData.outputAddresses.length > 5 ? '\n...' : ''}\`\`\``,
inline: false
});
}
// Send to Discord channel
await btcMonitorChannel.send({ embeds: [embed] });
} catch (error) {
console.error('Error processing Bitcoin transaction:', error);
}
};
// Register the transaction handler
btcSocket.onTransaction(transactionHandler);
return true;
} catch (error) {
console.error('Failed to initialize Bitcoin monitoring:', error);
return false;
}
}
async function processBitcoinTransaction(tx, blockexplorer) {
try {
// Process inputs with balances
const inputAddresses = tx.inputs
.filter(input => input.prev_out && input.prev_out.addr)
.map(input => input.prev_out.addr);
// Process outputs with balances
const outputAddresses = tx.out
.filter(output => output.addr)
.map(output => output.addr);
// Calculate total input value
const totalInputValue = tx.inputs.reduce((sum, input) => {
return sum + (input.prev_out ? (input.prev_out.value || 0) : 0);
}, 0);
// Calculate total output value
const totalOutputValue = tx.out.reduce((sum, output) => sum + (output.value || 0), 0);
// Calculate fee in satoshis
const feeSatoshis = totalInputValue - totalOutputValue;
// Calculate fee rate (sat/byte)
let feeRate = 'Unknown';
if (tx.size && feeSatoshis > 0) {
feeRate = Math.round(feeSatoshis / tx.size);
}
// Convert total output value to BTC (since this represents the transaction amount)
const totalValue = totalOutputValue / 100000000;
return {
hash: tx.hash,
amount: totalValue.toFixed(8),
size: tx.size || 'Unknown',
feeRate: feeRate,
fee: feeSatoshis > 0 ? feeSatoshis : 'Unknown',
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
timestamp: new Date()
};
} catch (error) {
console.error('Error processing transaction data:', error);
throw error;
}
}
function stopBitcoinMonitoring() {
if (btcSocket) {
isMonitoring = false;
// Properly close the WebSocket connection
try {
if (btcSocket.ws) {
if (btcSocket.ws.readyState === 1) { // WebSocket.OPEN = 1
btcSocket.ws.close();
}
// Force close and cleanup
btcSocket.ws.onmessage = null;
btcSocket.ws.onopen = null;
btcSocket.ws.onclose = null;
btcSocket.ws.onerror = null;
}
} catch (error) {
console.error('Error closing WebSocket:', error);
}
// Clear all references
btcSocket = null;
transactionHandler = null;
btcMonitorChannel = null;
minBtcAmount = 0; // Reset minimum amount
// Clear processed transactions to prevent duplicate detection issues
processedTransactions.clear();
return true;
}
return false;
}
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
console.log(`Command received: ${interaction.commandName}`);
try {
switch (interaction.commandName) {
case 'ping':
await interaction.reply('Pong!');
break;
case 'send':
const targetChannel = interaction.options.getChannel('channel');
const messageContent = interaction.options.getString('message');
if (!targetChannel.isTextBased()) {
await interaction.reply({ content: 'I can only send messages to text channels!', ephemeral: true });
return;
}
try {
await targetChannel.send(messageContent);
await interaction.reply({ content: `Message sent to ${targetChannel}!`, ephemeral: true });
} catch (error) {
console.error('Error sending message:', error);
await interaction.reply({ content: `Failed to send message: ${error.message}`, ephemeral: true });
}
break;
case 'search':
await interaction.deferReply();
const query = interaction.options.getString('query');
const mediaType = interaction.options.getString('type');
const count = interaction.options.getInteger('count') || 3;
try {
if (mediaType === 'image') {
// Use real search function
const results = await searchImages(query, count);
if (results.length === 0) {
await interaction.editReply(`❌ No image results found for "${query}". The search engines may not have returned any results for this query.`);
return;
}
// Create embeds for images (skip validation since we're getting real search results)
const embeds = results.map((result, i) => {
return new EmbedBuilder()
.setTitle(`${result.title}`)
.setImage(result.link)
.setColor(0x00AE86)
.setFooter({ text: `Search result ${i + 1} of ${results.length}` });
});
await interaction.editReply({
content: `🔍 Found ${results.length} images for "${query}":`,
embeds: embeds.slice(0, 10) // Discord allows max 10 embeds
});
}
else if (mediaType === 'video') {
// Use real video search function
const results = await searchVideos(query, count);
if (results.length === 0) {
await interaction.editReply(`❌ No video results found for "${query}". The search engines may not have returned any results for this query.`);
return;
}
// Create embeds for videos
const embeds = results.map((result, i) => {
const embed = new EmbedBuilder()
.setTitle(`${result.title}`)
.setURL(result.link)
.setColor(0xFF0000)
.setDescription(result.snippet || 'No description available');
// Add thumbnail for YouTube videos
if (result.link.includes('youtube.com') || result.link.includes('youtu.be')) {
const videoId = result.link.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/);
if (videoId) {
embed.setThumbnail(`https://img.youtube.com/vi/${videoId[1]}/maxresdefault.jpg`);
}
}
return embed;
});
await interaction.editReply({
content: `🔍 Found ${results.length} videos for "${query}":`,
embeds: embeds
});
}
} catch (error) {
console.error('Search error:', error);
await interaction.editReply(`❌ Error searching for "${query}": ${error.message}\n\nThis could be due to rate limiting or the search service being unavailable.`);
}
break;
case 'download':
await interaction.deferReply();
const fileUrl = interaction.options.getString('url');
const fileTitle = interaction.options.getString('title') || 'Downloaded file';
try {
// Validate the URL
const validation = await validateMultimediaUrl(fileUrl);
if (!validation.valid) {
await interaction.editReply(`Invalid or inaccessible multimedia URL: ${validation.error || 'Unknown error'}`);
return;
}
// Check file size (Discord has 8MB limit for regular users)
const fileSize = parseInt(validation.size || '0');
const maxSize = 8 * 1024 * 1024; // 8MB in bytes
if (fileSize > maxSize) {
await interaction.editReply({
content: `File is too large (${(fileSize / 1024 / 1024).toFixed(2)}MB). Discord limit is 8MB.`,
embeds: [new EmbedBuilder()
.setTitle(fileTitle)
.setURL(fileUrl)
.setDescription('Click the title to view the file directly')
.setColor(0xFFFF00)]
});
return;
}
// Download the file
const extension = getFileExtension(fileUrl) || '.bin';
const fileName = `download_${Date.now()}${extension}`;
const filePath = path.join(process.cwd(), fileName);
await downloadFile(fileUrl, filePath);
// Create attachment
const attachment = new AttachmentBuilder(filePath, { name: `${fileTitle}${extension}` });
// Create embed with file info
const embed = new EmbedBuilder()
.setTitle(fileTitle)
.setDescription(`Downloaded from: ${fileUrl}`)
.addFields(
{ name: 'Content Type', value: validation.contentType, inline: true },
{ name: 'File Size', value: `${(fileSize / 1024).toFixed(2)} KB`, inline: true }
)
.setColor(0x00FF00)
.setTimestamp();
await interaction.editReply({
content: 'File downloaded successfully!',
embeds: [embed],
files: [attachment]
});
// Clean up the temporary file
setTimeout(() => {
if (existsSync(filePath)) {
unlinkSync(filePath);
}
}, 5000); // Delete after 5 seconds
} catch (error) {
console.error('Download error:', error);
await interaction.editReply(`Error downloading file: ${error.message}`);
}
break;
case 'btc-monitor':
const action = interaction.options.getString('action');
const channel = interaction.options.getChannel('channel');
const minAmount = interaction.options.getNumber('min-amount');
try {
switch (action) {
case 'start':
if (!channel) {
await interaction.reply({
content: 'Please specify a channel to send Bitcoin updates to!',
ephemeral: true
});
return;
}
if (!channel.isTextBased()) {
await interaction.reply({
content: 'I can only send Bitcoin updates to text channels!',
ephemeral: true
});
return;
}
if (isMonitoring) {
await interaction.reply({
content: `Bitcoin monitoring is already running in ${btcMonitorChannel}!`,
ephemeral: true
});
return;
}
await interaction.deferReply({ ephemeral: true });
btcMonitorChannel = channel;
minBtcAmount = minAmount || 0; // Set minimum amount or default to 0
const success = await initBitcoinMonitoring();
if (success) {
isMonitoring = true;
await interaction.editReply(`✅ Bitcoin transaction monitoring started! Real-time transactions with minimum ${minBtcAmount} BTC will be posted to the specified channel.`);
// Send initial message to the monitoring channel
const startEmbed = new EmbedBuilder()
.setTitle('🟠 Bitcoin Transaction Monitor Started')
.setDescription(`Real-time Bitcoin transactions with minimum ${minBtcAmount} BTC will be displayed here.`)
.setColor(0xF7931A)
.setTimestamp();
await btcMonitorChannel.send({ embeds: [startEmbed] });
} else {
await interaction.editReply('❌ Failed to start Bitcoin monitoring. Please check the console for errors.');
}
break;
case 'stop':
if (!isMonitoring) {
await interaction.reply({
content: 'Bitcoin monitoring is not currently running!',
ephemeral: true
});
return;
}
const stopped = stopBitcoinMonitoring();
if (stopped) {
await interaction.reply({
content: '⏹️ Bitcoin transaction monitoring stopped.',
ephemeral: true
});
} else {
await interaction.reply({
content: '❌ Failed to stop Bitcoin monitoring.',
ephemeral: true
});
}
break;
case 'status':
const statusEmbed = new EmbedBuilder()
.setTitle('🟠 Bitcoin Monitor Status')
.addFields(
{ name: 'Status', value: isMonitoring ? '🟢 Running' : '🔴 Stopped', inline: true },
{ name: 'Channel', value: btcMonitorChannel ? btcMonitorChannel.toString() : 'None', inline: true },
{ name: 'Min Amount', value: `${minBtcAmount} BTC`, inline: true },
{ name: 'Processed TXs', value: `${processedTransactions.size}`, inline: true },
{ name: 'WebSocket', value: btcSocket ? '🟢 Connected' : '🔴 Disconnected', inline: true }
)
.setColor(isMonitoring ? 0x00FF00 : 0xFF0000)
.setTimestamp();
await interaction.reply({ embeds: [statusEmbed], ephemeral: true });
break;
}
} catch (error) {
console.error('Error handling btc-monitor command:', error);
await interaction.reply({
content: `Error with Bitcoin monitoring: ${error.message}`,
ephemeral: true
});
}
break;
}
} catch (error) {
console.error('Error handling command:', error);
// If the interaction hasn't been replied to yet
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({ content: 'An error occurred while executing this command!', ephemeral: true });
} else if (interaction.deferred) {
await interaction.editReply('An error occurred while executing this command!');
}
}
});
// Use environment variable for token (more secure)
client.login(process.env.DISCORD_TOKEN);