884 lines
37 KiB
JavaScript
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); |