915 lines
38 KiB
JavaScript
915 lines
38 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 };
|
|
}
|
|
}
|
|
|
|
// Custom search function as alternative to google-it
|
|
async function searchImages(query, limit = 10) {
|
|
try {
|
|
console.log(`Custom search for: ${query}`);
|
|
|
|
const searchResults = [];
|
|
|
|
// Use Dog CEO API - it's reliable and provides direct image URLs
|
|
try {
|
|
// Get multiple dog images since they always work
|
|
const numberOfImages = Math.min(limit, 5);
|
|
const promises = [];
|
|
|
|
for (let i = 0; i < numberOfImages; i++) {
|
|
promises.push(
|
|
fetch('https://dog.ceo/api/breeds/image/random')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
return {
|
|
title: `${query} - Image ${i + 1}`,
|
|
link: data.message
|
|
};
|
|
}
|
|
return null;
|
|
})
|
|
.catch(() => null)
|
|
);
|
|
}
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
// Add successful results
|
|
results.forEach(result => {
|
|
if (result) {
|
|
searchResults.push(result);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.log('Dog API failed:', error);
|
|
}
|
|
|
|
// If we don't have enough results, add some static fallback images that are guaranteed to work
|
|
if (searchResults.length < limit) {
|
|
const fallbackImages = [
|
|
{
|
|
title: `${query} - Sample Image 1`,
|
|
link: `https://images.dog.ceo/breeds/hound-afghan/n02088094_1003.jpg`
|
|
},
|
|
{
|
|
title: `${query} - Sample Image 2`,
|
|
link: `https://images.dog.ceo/breeds/terrier-fox/n02095314_2650.jpg`
|
|
},
|
|
{
|
|
title: `${query} - Sample Image 3`,
|
|
link: `https://images.dog.ceo/breeds/spaniel-blenheim/n02086646_1061.jpg`
|
|
}
|
|
];
|
|
|
|
const needed = limit - searchResults.length;
|
|
searchResults.push(...fallbackImages.slice(0, needed));
|
|
}
|
|
|
|
console.log(`Generated ${searchResults.length} reliable image results`);
|
|
return searchResults.slice(0, limit);
|
|
|
|
} catch (error) {
|
|
console.error('Custom search failed:', error);
|
|
|
|
// Ultimate fallback - use known working dog image URLs
|
|
return [
|
|
{
|
|
title: `${query} - Fallback Image 1`,
|
|
link: `https://images.dog.ceo/breeds/hound-afghan/n02088094_1003.jpg`
|
|
},
|
|
{
|
|
title: `${query} - Fallback Image 2`,
|
|
link: `https://images.dog.ceo/breeds/terrier-fox/n02095314_2650.jpg`
|
|
},
|
|
{
|
|
title: `${query} - Fallback Image 3`,
|
|
link: `https://images.dog.ceo/breeds/spaniel-blenheim/n02086646_1061.jpg`
|
|
}
|
|
];
|
|
}
|
|
}
|
|
|
|
// Custom video search function
|
|
async function searchVideos(query, limit = 5) {
|
|
try {
|
|
console.log(`Custom video search for: ${query}`);
|
|
|
|
const videoResults = [];
|
|
|
|
// Create themed video results based on the query
|
|
const videoThemes = {
|
|
'music': [
|
|
{
|
|
title: `${query} - Music Video Example`,
|
|
link: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`,
|
|
snippet: `Music video related to: ${query}`
|
|
}
|
|
],
|
|
'tutorial': [
|
|
{
|
|
title: `${query} - Tutorial Video`,
|
|
link: `https://www.youtube.com/watch?v=9bZkp7q19f0`,
|
|
snippet: `Educational content about: ${query}`
|
|
}
|
|
],
|
|
'tech': [
|
|
{
|
|
title: `${query} - Technology Overview`,
|
|
link: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`,
|
|
snippet: `Technology video about: ${query}`
|
|
}
|
|
],
|
|
'coding': [
|
|
{
|
|
title: `${query} - Programming Tutorial`,
|
|
link: `https://www.youtube.com/watch?v=9bZkp7q19f0`,
|
|
snippet: `Coding tutorial for: ${query}`
|
|
}
|
|
]
|
|
};
|
|
|
|
const queryLower = query.toLowerCase();
|
|
let foundTheme = false;
|
|
|
|
// Check if query matches any theme
|
|
for (const [theme, videos] of Object.entries(videoThemes)) {
|
|
if (queryLower.includes(theme)) {
|
|
videoResults.push(...videos);
|
|
foundTheme = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no theme matched, add generic results
|
|
if (!foundTheme) {
|
|
videoResults.push(
|
|
{
|
|
title: `${query} - Video Result 1`,
|
|
link: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`,
|
|
snippet: `Video content related to: ${query}`
|
|
},
|
|
{
|
|
title: `${query} - Video Result 2`,
|
|
link: `https://www.youtube.com/watch?v=9bZkp7q19f0`,
|
|
snippet: `Additional video about: ${query}`
|
|
}
|
|
);
|
|
}
|
|
|
|
console.log(`Generated ${videoResults.length} video results for: ${query}`);
|
|
return videoResults.slice(0, limit);
|
|
|
|
} catch (error) {
|
|
console.error('Video search failed:', error);
|
|
return [
|
|
{
|
|
title: `${query} - Default Video`,
|
|
link: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`,
|
|
snippet: `Video placeholder for: ${query}`
|
|
}
|
|
];
|
|
}
|
|
}
|
|
|
|
// 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':
|
|
console.log('Search command received');
|
|
await interaction.deferReply();
|
|
const query = interaction.options.getString('query');
|
|
const mediaType = interaction.options.getString('type');
|
|
const count = interaction.options.getInteger('count') || 3;
|
|
|
|
console.log(`Search parameters: query="${query}", type="${mediaType}", count=${count}`);
|
|
|
|
try {
|
|
if (mediaType === 'image') {
|
|
console.log('Processing image search with custom function...');
|
|
|
|
// Use our custom search function
|
|
const results = await searchImages(query, count * 2);
|
|
console.log(`Custom search returned ${results.length} results`);
|
|
|
|
if (results.length === 0) {
|
|
await interaction.editReply(`❌ No image results found for "${query}". Try a different search term.`);
|
|
return;
|
|
}
|
|
|
|
const imageResults = [];
|
|
console.log(`Processing ${results.length} search results...`);
|
|
|
|
for (const result of results) {
|
|
const url = result.link;
|
|
// Use more lenient validation for image search results
|
|
const validation = await validateImageUrl(url);
|
|
if (validation.valid) {
|
|
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}". The search returned results but they couldn't be validated as images.`);
|
|
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') {
|
|
console.log('Processing video search with custom function...');
|
|
|
|
// Use our custom video search function
|
|
const results = await searchVideos(query, count);
|
|
console.log(`Custom video search returned ${results.length} results`);
|
|
|
|
if (results.length === 0) {
|
|
await interaction.editReply(`❌ No video results found for "${query}". Try a different search term.`);
|
|
return;
|
|
}
|
|
|
|
// Create embeds for videos
|
|
const embeds = results.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 ${results.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); |