commit 63a562eba762f1cb42d45e4a595ba9d4ff709edb Author: ale Date: Sun Jun 8 02:04:05 2025 +0200 initial commit Signed-off-by: ale diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84a2610 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +package-lock.json +*.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..13e1736 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Discord Multimedia Bot + +A powerful Discord bot that can send messages and search for multimedia files on Google with enhanced display capabilities. + +## Features + +- 🏓 **Ping Command** - Simple ping command to check if the bot is working +- 📨 **Message Sending** - Send messages to any channel in the server +- 🔍 **Multimedia Search** - Search for images and videos with embedded preview +- âŦ‡ī¸ **File Download** - Download and share multimedia files directly in Discord + +## Installation + +1. Clone this repository +2. Install dependencies: + ```bash + npm install + ``` +3. Create a `.env` file with your bot token: + ```env + DISCORD_TOKEN=your_token_here + ``` +4. Run the bot: + ```bash + node index.js + ``` + +## Commands + +### 🏓 Ping +``` +/ping +``` +Simple response to check if the bot is online. + +### 📨 Send Message +``` +/send channel:#channel-name message:Your message here +``` +Sends a message to the specified channel. +- `channel`: The target channel (required) +- `message`: The message content (required) + +### 🔍 Search Media +``` +/search query:cute puppies type:image count:3 +``` +Searches for multimedia content on Google with enhanced display. +- `query`: What to search for (required) +- `type`: Either "image" or "video" (required) +- `count`: Number of results to show (1-5, default: 3) + +**Image Search Features:** +- Displays images directly in Discord with embeds +- Shows content type information +- Validates image URLs before displaying +- Supports JPG, PNG, GIF, and WebP formats + +**Video Search Features:** +- Creates rich embeds with video information +- Shows YouTube video thumbnails automatically +- Includes video descriptions when available +- Supports YouTube, Vimeo, and direct video files + +### âŦ‡ī¸ Download Files +``` +/download url:https://example.com/image.jpg title:My Image +``` +Downloads and shares multimedia files directly in Discord. +- `url`: Direct URL to the multimedia file (required) +- `title`: Optional title for the file (optional) + +**Download Features:** +- Validates multimedia URLs before downloading +- Checks file size (8MB Discord limit) +- Supports images, videos, and audio files +- Automatically cleans up temporary files +- Shows file information in rich embeds + +## Security Features + +- ✅ Environment variables for token security +- ✅ URL validation for multimedia content +- ✅ File size checking +- ✅ Temporary file cleanup +- ✅ Error handling and user feedback + +## Supported File Types + +**Images:** JPG, JPEG, PNG, GIF, WebP, BMP, SVG +**Videos:** MP4, WebM, MOV, AVI, MKV, FLV +**Audio:** MP3, WAV, OGG, M4A, AAC, FLAC + +## Technical Implementation + +- **dotenv** for secure environment variable management +- **Enhanced fetch** with proper headers and error handling +- **Discord.js v14** with modern slash commands +- **Google-it** for search functionality +- **Stream pipeline** for efficient file downloads +- **Rich embeds** for multimedia display + +## Security Note + +âš ī¸ **Important:** Keep your bot token private and never commit it to a public repository. Always use environment variables for sensitive data. + +## Error Handling + +The bot includes comprehensive error handling for: +- Invalid URLs +- Network timeouts +- File size limitations +- Permission errors +- API rate limits + +## Performance Features + +- Efficient file streaming for downloads +- Automatic temporary file cleanup +- URL validation before processing +- Content type verification +- Background processing for long operations diff --git a/index.js b/index.js new file mode 100644 index 0000000..78c550b --- /dev/null +++ b/index.js @@ -0,0 +1,612 @@ +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; + +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 + } + ] + } + ]; + + 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 { + btcSocket = new Socket(); + + btcSocket.onTransaction(async (tx) => { + if (!btcMonitorChannel || !isMonitoring) return; + + try { + // Process transaction data + const txData = await processBitcoinTransaction(tx, blockexplorer); + + // 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 Rate', value: `${txData.feeRate} sat/byte`, inline: true } + ) + .setTimestamp() + .setFooter({ text: 'Live Bitcoin Network' }); + + // 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); + } + }); + + console.log('Bitcoin monitoring initialized successfully'); + 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 transaction value + const totalValue = tx.out.reduce((sum, output) => sum + (output.value || 0), 0) / 100000000; + + // Calculate fee rate (if available) + const feeRate = tx.fee && tx.size ? Math.round(tx.fee / tx.size) : 'Unknown'; + + return { + hash: tx.hash, + amount: totalValue.toFixed(8), + size: tx.size || 'Unknown', + feeRate: feeRate, + inputAddresses: inputAddresses, + outputAddresses: outputAddresses, + timestamp: new Date() + }; + } catch (error) { + console.error('Error processing transaction data:', error); + throw error; + } +} + +function stopBitcoinMonitoring() { + if (btcSocket) { + isMonitoring = false; + btcSocket = null; + btcMonitorChannel = null; + console.log('Bitcoin monitoring stopped'); + 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'); + + 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; + const success = await initBitcoinMonitoring(); + + if (success) { + isMonitoring = true; + await interaction.editReply('✅ Bitcoin transaction monitoring started! Real-time transactions 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 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 } + ) + .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); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..41a1322 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "discordbot", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "author": "ale", + "license": "MIT", + "private": true, + "dependencies": { + "blockchain.info": "^2.12.1", + "discord.js": "^14.18.0", + "dotenv": "^16.5.0", + "google-it": "^1.6.4", + "node-fetch": "^2.7.0" + } +} diff --git a/test-bot.sh b/test-bot.sh new file mode 100755 index 0000000..d81119e --- /dev/null +++ b/test-bot.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Discord Bot Test Script +# This script demonstrates how to test the bot functionality + +echo "🤖 Discord Multimedia Bot - Test Guide" +echo "=======================================" +echo "" + +echo "📋 Available Commands:" +echo "" +echo "1. /ping" +echo " - Tests if the bot is responsive" +echo " - Expected response: 'Pong!'" +echo "" + +echo "2. /send channel:#general message:Hello World!" +echo " - Sends a message to the specified channel" +echo " - Make sure the bot has permission to send messages" +echo "" + +echo "3. /search query:cats type:image count:2" +echo " - Searches for cat images" +echo " - Will display embedded images directly in Discord" +echo "" + +echo "4. /search query:funny videos type:video count:3" +echo " - Searches for funny videos" +echo " - Will show video links with thumbnails" +echo "" + +echo "5. /download url:https://example.com/image.jpg title:Test Image" +echo " - Downloads and shares a multimedia file" +echo " - File must be under 8MB for Discord" +echo "" + +echo "🔧 Bot Status:" +if pgrep -f "node index.js" > /dev/null; then + echo "✅ Bot is currently running" + echo "📊 Process ID: $(pgrep -f 'node index.js')" +else + echo "❌ Bot is not running" + echo "💡 Start with: node index.js" +fi + +echo "" +echo "📁 Project Structure:" +ls -la /home/ale/projects/discord/discordbot/ | grep -E '\.(js|json|md|env)$' + +echo "" +echo "🌐 Environment:" +if [ -f "/home/ale/projects/discord/discordbot/.env" ]; then + echo "✅ .env file exists" +else + echo "❌ .env file missing - create it with DISCORD_TOKEN=your_token" +fi + +echo "" +echo "đŸ“Ļ Dependencies:" +if [ -f "/home/ale/projects/discord/discordbot/package.json" ]; then + echo "✅ package.json exists" + echo "📋 Installed packages:" + cd /home/ale/projects/discord/discordbot && npm list --depth=0 2>/dev/null | grep -E '(discord.js|dotenv|google-it|node-fetch)' +else + echo "❌ package.json missing" +fi