discordbot/index.js
ale 618553a205
get out all console log
Signed-off-by: ale <ale@manalejandro.com>
2025-06-08 03:06:33 +02:00

694 lines
29 KiB
JavaScript

import { config } from 'dotenv';
import { Client, Events, GatewayIntentBits, SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } from 'discord.js';
import googleIt from 'google-it';
import fetch from 'node-fetch';
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 });
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 };
}
}
// 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;
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') {
// Enhanced image search with better filtering
const searchTerm = `${query} filetype:jpg OR filetype:png OR filetype:gif OR filetype:webp`;
const results = await googleIt({ query: searchTerm, limit: count * 5 });
const imageResults = [];
for (const result of results) {
const url = result.link;
// Validate if the URL is actually an image
const validation = await validateMultimediaUrl(url);
if (validation.valid && validation.contentType.startsWith('image/')) {
imageResults.push({
title: result.title,
url: url,
contentType: validation.contentType
});
if (imageResults.length >= count) break;
}
}
if (imageResults.length === 0) {
await interaction.editReply(`No valid image results found for "${query}"`);
return;
}
// Create embeds for images
const embeds = imageResults.map((result, i) => {
return new EmbedBuilder()
.setTitle(`Image ${i + 1}: ${result.title}`)
.setImage(result.url)
.setColor(0x00AE86)
.setFooter({ text: `Content-Type: ${result.contentType}` });
});
await interaction.editReply({
content: `Found ${imageResults.length} images for "${query}":`,
embeds: embeds.slice(0, 10) // Discord allows max 10 embeds
});
}
else if (mediaType === 'video') {
// Enhanced video search
const searchTerm = `${query} filetype:mp4 OR filetype:webm OR site:youtube.com OR site:vimeo.com`;
const results = await googleIt({ query: searchTerm, limit: count * 3 });
const videoResults = results
.filter(result => {
const url = result.link.toLowerCase();
return url.includes('youtube.com') || url.includes('youtu.be') ||
url.includes('vimeo.com') || url.endsWith('.mp4') ||
url.endsWith('.webm') || url.includes('video');
})
.slice(0, count);
if (videoResults.length === 0) {
await interaction.editReply(`No video results found for "${query}"`);
return;
}
// Create embeds for videos
const embeds = videoResults.map((result, i) => {
const embed = new EmbedBuilder()
.setTitle(`Video ${i + 1}: ${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 ${videoResults.length} videos for "${query}":`,
embeds: embeds
});
}
} catch (error) {
console.error('Search error:', error);
await interaction.editReply(`Error searching for "${query}": ${error.message}`);
}
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);