diff --git a/CODE_REVIEW.md b/CODE_REVIEW.md new file mode 100644 index 0000000..ee79618 --- /dev/null +++ b/CODE_REVIEW.md @@ -0,0 +1,455 @@ +# Code Review & Suggested Fixes for iceC Discord Bot + +## 🔴 Critical Issues + +### 1. **Bot/Client Mismatch - Commands Won't Work** +**Location:** Lines 43, and all command decorators + +**Issue:** Using `discord.Client()` but commands require `commands.Bot()` to work. All `@commands.command()` decorated functions won't be registered. + +**Fix:** +```python +# Line 43: Change from +client = discord.Client(command_prefix='!', intents=intents, case_insensitive=True) + +# To: +from discord.ext import commands +bot = commands.Bot(command_prefix='!', intents=intents, case_insensitive=True) +client = bot # For backwards compatibility if needed +``` + +**Impact:** HIGH - None of your commands will work currently. + +--- + +### 2. **Commands Not Registered with Bot** +**Location:** Throughout file + +**Issue:** Commands are defined but never added to the bot. Need to either: +- Use `commands.Bot()` instead of `Client()` (see issue #1) +- Or manually add commands using `bot.add_command()` or `bot.load_extension()` + +**Fix:** Ensure using `commands.Bot()` (see above) + +--- + +### 3. **Mixed Discord Library Usage** +**Location:** Lines 3, 6, 11, throughout + +**Issue:** Mixing `discord` and `nextcord` libraries inconsistently. Should choose one and use it consistently. + +**Recommendation:** Use `nextcord` throughout since you're already using it more heavily. Remove `discord` imports where possible, or replace with `nextcord`. + +**Fix:** +- Replace all `discord.` references with `nextcord.` +- Or standardize on `discord.py` (which is more maintained) + +--- + +### 4. **Logging Formatter Bug** +**Location:** Line 22 + +**Issue:** Typo in logging formatter: `&(message)s` should be `%(message)s` + +**Fix:** +```python +# Line 22: Change from +handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:&(message)s')) + +# To: +handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(message)s')) +``` + +--- + +### 5. **Duplicate Event Handler** +**Location:** Lines 127 and 133 + +**Issue:** `on_command_error` is defined twice. Second definition will override the first. + +**Fix:** Combine both error handlers into one: +```python +@bot.event # or @client.event +async def on_command_error(ctx: commands.Context, error): + if isinstance(error, commands.CommandOnCooldown): + em = nextcord.Embed(description=f'**Cooldown active**\ntry again in `{error.retry_after:.2f}`s*', color=embed_color) + await ctx.send(embed=em) + elif isinstance(error, commands.MissingRequiredArgument): + await ctx.send(embed=nextcord.Embed(description="Missing `arguments`", color=embed_color)) + # Add other error types as needed +``` + +--- + +### 6. **Unban Command Type Error** +**Location:** Line 700 + +**Issue:** Function signature has `member: discord.Member` but then uses `member.split('#')` which expects a string. + +**Fix:** +```python +# Change from +async def unban(self, ctx, *, member: discord.Member): + +# To: +async def unban(ctx, *, member): + # member should be passed as string like "username#discriminator" + member_name, member_discriminator = member.split('#') +``` + +--- + +### 7. **Node Connection Never Called** +**Location:** Lines 103-105 + +**Issue:** `node_connect()` function is defined but never called, so wavelink won't connect. + +**Fix:** Add to `on_ready()` or call it explicitly: +```python +@client.event +async def on_ready(): + print(f'We have logged in as {client.user}') + await node_connect() # Add this +``` + +--- + +## 🟠 Important Issues + +### 8. **Bare Exception Handling** +**Location:** Lines 122, 158, 293, 344, and others + +**Issue:** Using bare `except:` or `except Exception:` without proper error handling/logging. + +**Fix:** Always catch specific exceptions and log them: +```python +# Instead of: +except Exception: + return '' + +# Use: +except SpecificError as e: + logger.error(f"Error occurred: {e}") + return None +``` + +--- + +### 9. **Unused/Redundant Variables** +**Location:** Multiple locations + +**Issues:** +- Line 38: `all_intents = intents.all()` followed by `all_intents = True` - redundant +- Line 40: `intent = discord.Intents.default()` - never used +- Line 44: `global user_arr, user_dict` - declared globally but not needed +- Line 46: `user_arr = np.array([])` - initialized but rarely used effectively +- Line 318: `global user_list` - unnecessary global declaration + +**Fix:** Remove unused variables and properly scope necessary ones. + +--- + +### 10. **Undefined Variable Usage** +**Location:** Line 164 + +**Issue:** `song_count` is used but may not be defined when `loopqueue_command` is called before `queue_command`. + +**Fix:** Calculate `song_count` in the function or check if it exists: +```python +song_count = len(vc.queue) if hasattr(vc, 'queue') else 0 +``` + +--- + +### 11. **Incorrect Logic in Resume Command** +**Location:** Lines 248-254 + +**Issue:** Logic check `if vc.is_playing()` then checking `if vc.is_paused()` is backwards. If it's playing, it can't be paused. + +**Fix:** +```python +if vc.is_paused(): # Check if paused first + await vc.resume() + await ctx.send(embed=nextcord.Embed(description='Music `RESUMED`!', color=embed_color)) +elif vc.is_playing(): + await ctx.send(embed=nextcord.Embed(description='Already in `RESUMED State`', color=embed_color)) +``` + +--- + +### 12. **Incorrect Comparison in Skipto Command** +**Location:** Line 430 + +**Issue:** `elif position == vc.queue._queue[position-1]:` compares an int with a track object. + +**Fix:** Remove this check or fix the logic: +```python +# This check doesn't make sense, remove or rewrite +# The position check already ensures validity +``` + +--- + +### 13. **Empty String Returns** +**Location:** Lines 157, 159 + +**Issue:** Returning empty strings `''` instead of `None` or `return`. + +**Fix:** Use `return` or `return None`. + +--- + +### 14. **Inconsistent Error Handling in Binance Functions** +**Location:** Lines 584-600, 603-611, etc. + +**Issue:** No error handling for API calls that could fail (network issues, invalid API keys, rate limits). + +**Fix:** Add try/except blocks around Binance API calls. + +--- + +### 15. **String Formatting Issues in Mute Command** +**Location:** Line 725 + +**Issue:** `.format(member, ctx.message.author, color=0xff00f6)` - incorrect format string usage. + +**Fix:** +```python +embed=discord.Embed(title='User muted!', description=f'**{member}** was muted by **{ctx.message.author}**!', color=0xff00f6) +``` + +--- + +### 16. **Unused Attribute** +**Location:** Line 47 + +**Issue:** `setattr(wavelink.Player, 'lq', False)` sets class attribute but might not be necessary. + +**Fix:** Consider using instance attributes instead or remove if not needed. + +--- + +## 🟡 Code Quality & Best Practices + +### 17. **File Organization** +**Issue:** Everything is in one large file (742 lines). Hard to maintain. + +**Recommendation:** +- Split into cogs (as mentioned in TODO.txt) +- Organize by functionality (music, moderation, trading, etc.) +- Use a proper project structure + +**Suggested Structure:** +``` +/ + main.py (bot initialization) + cogs/ + music.py + moderation.py + trading.py + utils/ + helpers.py + config.py +``` + +--- + +### 18. **Magic Numbers** +**Location:** Lines 633, 389, etc. + +**Issue:** Hardcoded values (40.0, -1.0, 0.4, etc.) should be constants. + +**Fix:** +```python +MARGIN_RATIO_THRESHOLD = 40.0 +PROFIT_THRESHOLD = -1.0 +LIQUIDATION_RATIO_THRESHOLD = 0.4 +``` + +--- + +### 19. **Global State Management** +**Location:** Throughout + +**Issue:** Using global variables (`user_dict`, `user_arr`, `FAV_LIST`) instead of bot attributes. + +**Fix:** Store in bot instance: +```python +bot.user_dict = {} +bot.fav_list = {} +``` + +--- + +### 20. **Inconsistent Naming** +**Location:** Throughout + +**Issues:** +- Mixing `snake_case` and inconsistent naming +- Some functions end with `_command` (good for avoiding conflicts) but not all + +**Recommendation:** Use consistent naming convention. + +--- + +### 21. **Missing Type Hints** +**Location:** Throughout + +**Issue:** Functions lack proper type hints, making code harder to understand. + +**Fix:** Add type hints to function signatures. + +--- + +### 22. **No Input Validation** +**Location:** Trading commands, volume command, etc. + +**Issue:** Limited validation on user inputs (e.g., volume could be negative or string). + +**Fix:** Add proper validation before processing. + +--- + +### 23. **Security Concerns** + +**Location:** Lines 105, 626 + +**Issues:** +- Hardcoded server endpoint (line 105) +- Potential for API key exposure if not using .env properly +- No validation of user permissions in some commands + +**Fix:** +- Move all sensitive data to .env +- Validate permissions before executing sensitive operations +- Use environment variables for all credentials + +--- + +### 24. **Memory Leaks** +**Location:** Line 321-324 + +**Issue:** Recreating numpy arrays on each `nowplaying` command call. Inefficient. + +**Fix:** Cache or use more efficient data structures. + +--- + +### 25. **Missing Documentation** +**Location:** Throughout + +**Issue:** Limited docstrings and comments explaining complex logic. + +**Fix:** Add docstrings to all functions and explain complex algorithms. + +--- + +### 26. **Dependencies File Missing** + +**Issue:** No `requirements.txt` file, making installation difficult for others. + +**Fix:** Create `requirements.txt`: +``` +discord.py>=2.0.0 +nextcord>=2.0.0 +python-dotenv>=1.0.0 +python-binance>=1.0.0 +wavelink>=2.0.0 +numpy>=1.24.0 +lyricsgenius>=5.0.0 +``` + +--- + +### 27. **Inconsistent Import Organization** +**Location:** Lines 1-17 + +**Issue:** Imports not organized by standard (stdlib, third-party, local). + +**Fix:** Organize imports: +```python +# Standard library +import os +import json +import logging +import datetime +import random +from typing import Optional + +# Third-party +import discord +from discord.ext import commands, tasks +import nextcord +from nextcord.ext import commands +import wavelink +from wavelink.ext import spotify +import numpy as np +import binance +from binance import Client, ThreadedWebsocketManager, ThreadedDepthCacheManager +import lyricsgenius +from dotenv import load_dotenv, find_dotenv +``` + +--- + +### 28. **Task Not Started** +**Location:** Line 656 + +**Issue:** `futures_position_alerts.start()` is commented out, so the task never runs. + +**Fix:** Uncomment and ensure proper error handling. + +--- + +## 🟢 Minor Improvements + +### 29. **Code Comments** +- Remove commented out code (lines 544-579, 658-674) +- Add meaningful comments for complex logic +- Remove TODO comments from production code + +### 30. **String Formatting** +- Use f-strings consistently (already mostly done) +- Ensure all user-facing messages are properly formatted + +### 31. **Error Messages** +- Make error messages more user-friendly +- Add suggestions for common errors + +### 32. **Code Duplication** +- Extract common patterns (embed creation, error responses) +- Use helper functions for repeated logic + +--- + +## Priority Fix Order + +1. **Fix Bot/Client issue** (Critical - nothing works without this) +2. **Fix logging formatter bug** (Quick fix) +3. **Fix duplicate event handler** (Quick fix) +4. **Add error handling** (Prevents crashes) +5. **Fix type errors** (unban, resume logic) +6. **Organize code structure** (Long-term maintenance) + +--- + +## Testing Recommendations + +- Test all commands after fixing Bot/Client issue +- Test error scenarios (network failures, invalid inputs) +- Test with multiple users in voice channels +- Test Binance API error handling +- Performance testing for large queues + +--- + +## Summary + +**Critical Issues:** 7 (must fix immediately) +**Important Issues:** 9 (should fix soon) +**Code Quality Issues:** 11 (improve over time) +**Minor Issues:** 4 (nice to have) + +**Total Issues Found:** 31 + +The most critical issue is the Bot/Client mismatch - your commands won't work until this is fixed. After that, focus on error handling and code organization. diff --git a/README.md b/README.md index 539cca2..e15c24f 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,266 @@ # iceC Discord Bot -A discord bot template im working on as I explore the mystical lands of discord.py -I will be adding stuff bit by bit, day by day. +A feature-rich Discord bot built with Python, featuring music playback, cryptocurrency trading alerts, and moderation tools. +## 🎯 Features +### 🎵 Music Bot Features +- **YouTube Music Playback** - Play songs from YouTube in voice channels +- **Spotify Playlist Support** - Play entire Spotify playlists +- **Queue Management** - Add, remove, shuffle, and manage song queues +- **Playback Controls** - Play, pause, resume, skip, seek, and volume control +- **Loop Functions** - Loop single songs or entire queues +- **Now Playing** - View current track information and requester -Download the zip file and upload it on [VirusTotal](https://www.virustotal.com) if you are doubtful. +### 📊 Trading Features (Binance Integration) +- **Position Alerts** - Automated alerts for futures positions +- **Margin Ratio Monitoring** - Get notified when margin ratios exceed thresholds +- **Favorite List** - Track favorite cryptocurrencies for SPOT and FUTURES +- **Account Balance** - View futures account balance +### 🛡️ Moderation Features +- **User Management** - Kick, ban, unban, mute, and unmute users +- **Role Management** - Assign roles to users +- **Permission-based Commands** - Role-based access control -If you dont know how to download the repository as a zip file press the green button as below. +## 📋 Prerequisites -![image](https://user-images.githubusercontent.com/35976946/218763761-e3baa46c-d23b-4b4a-8bdb-d971bfababa0.png) +- Python 3.8 or higher +- Discord Bot Token ([Get one here](https://discord.com/developers/applications)) +- Binance API Key and Secret (for trading features - optional) +- Spotify Client ID and Secret (for Spotify features - optional) +- A Lavalink server (for music features) -After pressing the button press on 'Download ZIP'. The very last button. -![image](https://user-images.githubusercontent.com/35976946/218764456-f9b62665-01c4-4007-8608-0d13a2816f61.png) -- Link Recently changed to - [ArliT1-F](https://github.com/ArliT1-F/iceC) +## 🚀 Installation + +### 1. Clone the Repository -### Manual Installation ```bash git clone https://github.com/ArliT1-F/iceC.git cd iceC ``` -After the zip is downloaded just drag it in the virus total web page and wait for it to be scanned. +### 2. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 3. Environment Setup + +Create a `.env` file in the root directory with the following variables: + +```env +# Discord Bot Configuration +DISCORD_TOKEN=your_discord_bot_token_here +CHANNEL_ID=your_channel_id_here + +# Binance API (Optional - for trading features) +BINANCE_API_KEY=your_binance_api_key +BINANCE_API_SECRET=your_binance_api_secret + +# Spotify API (Optional - for Spotify playlist support) +spotify_id=your_spotify_client_id +spotify_secret=your_spotify_client_secret + +# Lavalink Server (Required for music features) +LAVALINK_HOST=node1.kartadharta.xyz +LAVALINK_PORT=443 +LAVALINK_PASSWORD=your_lavalink_password_here +``` + +### 4. Configuration Files + +Ensure `FAV_LIST.json` exists with the following structure: + +```json +{ + "FUTURES": {}, + "SPOT": {} +} +``` + +## 📖 Usage + +### Starting the Bot + +```bash +python main.py +``` + +The bot will connect to Discord and be ready to use once you see the "We have logged in as..." message. + +### Command Prefix + +The bot uses `!` as the default command prefix. + +## 🎮 Commands + +### Music Commands + +| Command | Aliases | Description | Example | +|---------|---------|-------------|---------| +| `!play` | `!p` | Play a song from YouTube | `!play never gonna give you up` | +| `!splay` | `!sp` | Play a Spotify playlist | `!sp https://open.spotify.com/playlist/...` | +| `!pause` | `!stop` | Pause the current track | `!pause` | +| `!resume` | - | Resume the paused track | `!resume` | +| `!skip` | `!next`, `!s` | Skip to the next track | `!skip` | +| `!nowplaying` | `!np` | Show current track information | `!np` | +| `!queue` | `!q`, `!track` | Display the current queue | `!queue` | +| `!loop` | - | Loop/unloop the current song | `!loop` | +| `!loopqueue` | `!lq` | Enable/disable queue looping | `!lq start` | +| `!volume` | `!vol` | Set the player volume (0-100) | `!vol 50` | +| `!seek` | - | Seek to position in track (seconds) | `!seek 120` | +| `!shuffle` | `!mix` | Shuffle the queue | `!shuffle` | +| `!del` | `!remove`, `!drop` | Delete a track from queue | `!del 3` | +| `!skipto` | `!goto` | Skip to a specific track in queue | `!skipto 5` | +| `!move` | `!set` | Move a track to a different position | `!move 2 5` | +| `!clear` | - | Clear the entire queue | `!clear` | +| `!disconnect` | `!dc`, `!leave` | Disconnect from voice channel | `!dc` | +| `!save` | `!dm` | DM current song or queue | `!save` or `!save queue` | + +### Trading Commands + +| Command | Description | Example | +|---------|-------------|---------| +| `!add_fav` | Add a symbol to favorites | `!add_fav FUT BTCUSDT` | +| `!favs` | List favorite cryptocurrencies | `!favs` | +| `!fubln` | Show futures account balance | `!fubln` | + +### Moderation Commands + +| Command | Description | Permissions Required | +|---------|-------------|---------------------| +| `!kick` | Kick a user from the server | Administrator, Kick Members | +| `!ban` | Ban a user from the server | Administrator, Ban Members | +| `!unban` | Unban a user | Administrator, Ban Members | +| `!mute` | Mute a user | Manage Messages | +| `!unmute` | Unmute a user | Manage Messages | +| `!setrole` | Assign a role to a user | Administrator | + +### Utility Commands + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!ping` | - | Check bot latency | +| `!info` | `!i` | Show bot information (Owner only) | + +## 🔧 Configuration + +### Lavalink Server + +The bot connects to a Lavalink server for music playback. By default, it uses: +- Host: `node1.kartadharta.xyz` +- Port: `443` +- Protocol: HTTPS + +You can modify the connection in the `node_connect()` function in `main.py`. + +### Required Intents + +Ensure your bot has the following intents enabled in the Discord Developer Portal: +- ✅ Server Members Intent +- ✅ Message Content Intent +- ✅ Voice States Intent +- ✅ Emojis and Stickers Intent + +## 📁 Project Structure + +``` +iceC/ +├── main.py # Main bot file +├── requirements.txt # Python dependencies +├── FAV_LIST.json # Trading favorites list +├── .env # Environment variables (create this) +├── discord.log # Bot logs (created automatically) +├── LICENSE # GNU GPL v3 License +└── README.md # This file +``` + +## 🐛 Troubleshooting + +### Bot Commands Not Working +- Ensure you're using `commands.Bot()` (already fixed in code) +- Check that the bot has proper permissions in your server +- Verify the command prefix is correct (`!` by default) + +### Music Not Playing +- Ensure you have a Lavalink server running and accessible +- Check that the bot has permission to join voice channels +- Verify the node connection in `node_connect()` + +### Trading Features Not Working +- Ensure Binance API keys are correctly set in `.env` +- Check that API keys have necessary permissions (read-only recommended) +- Verify `FAV_LIST.json` exists and has correct structure + +### Permission Errors +- Ensure the bot has necessary roles/permissions: + - Send Messages + - Connect to Voice Channels + - Speak in Voice Channels + - Manage Roles (for moderation commands) + +## 🔐 Security Notes + +- **Never commit your `.env` file** - It contains sensitive tokens +- Use read-only API keys when possible +- Regularly rotate your Discord bot token +- Keep dependencies updated for security patches + +## 📝 Environment Variables + +All required environment variables: + +| Variable | Required | Description | +|----------|----------|-------------| +| `DISCORD_TOKEN` | ✅ Yes | Your Discord bot token | +| `CHANNEL_ID` | ✅ Yes | Channel ID for alerts | +| `LAVALINK_PASSWORD` | ✅ Yes* | Password for Lavalink server (required for music) | +| `LAVALINK_HOST` | ❌ Optional | Lavalink server host (default: node1.kartadharta.xyz) | +| `LAVALINK_PORT` | ❌ Optional | Lavalink server port (default: 443) | +| `BINANCE_API_KEY` | ❌ Optional | Binance API key for trading | +| `BINANCE_API_SECRET` | ❌ Optional | Binance API secret | +| `spotify_id` | ❌ Optional | Spotify client ID | +| `spotify_secret` | ❌ Optional | Spotify client secret | + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## 📄 License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. + +## 🔗 Links + +- **Repository**: [GitHub - ArliT1-F/iceC](https://github.com/ArliT1-F/iceC) +- **Discord Developer Portal**: [discord.com/developers](https://discord.com/developers) +- **Binance API**: [binance.com/api](https://binance.com/api) +- **Wavelink Documentation**: [wavelink.readthedocs.io](https://wavelink.readthedocs.io/) + +## ⚠️ Disclaimer + +- This bot is for educational purposes +- Trading features involve financial risks - use at your own discretion +- Ensure compliance with Discord's Terms of Service +- Respect copyright when using music features + +## 📞 Support + +If you encounter any issues or have questions: +1. Check the [Troubleshooting](#-troubleshooting) section +2. Review the code comments and documentation +3. Open an issue on GitHub + +--- + +**Made with ❤️ by the iceC development team** + +*Last updated: 2024* diff --git a/TO-DO.txt b/TO-DO.txt index ddc1c8c..d227e73 100644 --- a/TO-DO.txt +++ b/TO-DO.txt @@ -1,7 +1,146 @@ -The to do list for iceC bot. +# iceC Bot - TO-DO List & Ideas -1. figure out how .env's work and use it to hide the bot token --- Solved (i am very stupid. 2024 me) -2. implement cogs for ease of use -3. make ffmpeg music commands ---obsolete (due to youtube_dl having huge problems atm. on hiatus until it gets fixed.) -4. maybe a small and simple game, or something fun like: the bot reads the chat and saves the most used lines from the users and appends them in a list. - this is so whenever you tag the bot or say its name, it will pick lines from the list and send one of them in the chat randomly. --- On Hold +## ✅ Completed Tasks + +### 1. Environment Variables (.env) +- ✅ **SOLVED** - Bot now properly uses `.env` file to hide sensitive tokens and API keys +- ✅ All configuration moved to environment variables for security +- ✅ Added `.env` template in README.md + +--- + +## 🚧 In Progress / Planned + +### 2. Implement Cogs for Code Organization +**Status:** Not Started +**Priority:** High +**Reason:** Codebase is getting large (750+ lines in single file). Cogs will make it more maintainable. + +**Suggested Cog Structure:** +``` +cogs/ +├── music.py # All music commands (play, pause, queue, etc.) +├── trading.py # Binance trading features (add_fav, favs, fubln) +├── moderation.py # Moderation commands (kick, ban, mute, etc.) +└── utility.py # Utility commands (ping, info, etc.) +``` + +**Benefits:** +- Better code organization +- Easier to maintain and debug +- Can reload cogs without restarting entire bot +- Team collaboration friendly + +--- + +## 💡 Ideas & Feature Requests + +### 3. FFmpeg Music Commands +**Status:** Obsolete / Canceled +**Reason:** YouTube-DL had major issues. Bot now uses Wavelink which is more stable and feature-rich. +**Current Solution:** ✅ Using Wavelink for music playback (better alternative) + +--- + +### 4. Chat Learning & Response Bot (Fun Game Feature) +**Status:** On Hold +**Priority:** Low-Medium +**Idea:** Bot reads chat messages and saves frequently used phrases/lines from users. When bot is tagged or someone says its name, it randomly responds with one of the saved phrases. + +**Implementation Suggestions:** +- Store phrases in JSON file (e.g., `chat_phrases.json`) +- Track message frequency (maybe top 10-20 most used phrases) +- Filter out commands and common Discord messages +- Add cooldown to prevent spam (maybe 1 response per 5 minutes) +- Allow admins to clear/reset phrases +- Optional: Add sentiment or context filtering + +**Commands to Add:** +- `!learn` - Manually add a phrase (admin only?) +- `!phrases` - Show saved phrases +- `!forget` - Clear saved phrases (admin only) +- `!toggle-learn` - Enable/disable learning (admin only) + +**Technical Notes:** +- Use message content intent (already enabled) +- Store phrases per server (guild-specific) +- Consider rate limiting to avoid learning spam +- Maybe add minimum phrase length (3+ words?) + +--- + +## 📋 Additional Suggestions + +### 5. Error Handling Improvements +**Status:** ✅ Mostly Done +**Notes:** Added better error handling and logging. Could add more specific error messages for users. + +### 6. Documentation +**Status:** ✅ Completed +**Notes:** README.md has been updated with comprehensive documentation. + +### 7. Testing +**Priority:** Medium +**Idea:** Add unit tests for core functionality (queue management, command parsing, etc.) + +### 8. Configuration File +**Priority:** Low +**Idea:** Create a `config.py` or `config.json` for customizable settings (command prefix, default volume, etc.) + +### 9. Database Integration +**Priority:** Low +**Idea:** Replace JSON files with a proper database (SQLite for small scale, PostgreSQL for larger deployments) +- User preferences (favorite volume, etc.) +- Chat learning phrases +- Server-specific settings +- Playlist storage + +### 10. Music Features Enhancement +**Priority:** Medium +**Ideas:** +- Playlist saving/loading (user-specific) +- Search history +- Song recommendations based on queue +- Lyrics integration (partially commented out in code) + +### 11. Trading Features Enhancement +**Priority:** Low +**Ideas:** +- Price alerts (notify when crypto hits certain price) +- Portfolio tracking +- Trading statistics +- Multiple exchange support + +### 12. Command Aliases Enhancement +**Priority:** Low +**Idea:** Allow server admins to customize command aliases per server + +--- + +## 🐛 Known Issues / Fixes Needed + +### Fixed in Latest Update: +- ✅ Bot/Client mismatch (fixed - now uses commands.Bot) +- ✅ Logging formatter bug +- ✅ Duplicate event handlers +- ✅ Undefined variable errors +- ✅ Type errors in unban command +- ✅ Logic errors in resume command + +### Remaining Minor Issues: +- Mixed discord/nextcord usage (could standardize on one library) +- Some global variables could be moved to bot instance attributes +- Code structure could be improved with cogs (see item #2) + +--- + +## 📝 Notes + +- Keep this file updated as features are completed +- Add new ideas or feature requests here +- Mark completed items with ✅ +- Use priority tags: High, Medium, Low + +--- + +**Last Updated:** 2024 diff --git a/cogs/__init__.py b/cogs/__init__.py new file mode 100644 index 0000000..19caef1 --- /dev/null +++ b/cogs/__init__.py @@ -0,0 +1 @@ +# Cogs package for iceC Discord Bot diff --git a/cogs/moderation.py b/cogs/moderation.py new file mode 100644 index 0000000..f670e64 --- /dev/null +++ b/cogs/moderation.py @@ -0,0 +1,104 @@ +""" +Moderation cog for iceC Discord Bot +Handles moderation commands like kick, ban, mute, etc. +""" +import discord +from discord.ext import commands +from discord.ext.commands import has_permissions +import nextcord + + +class Moderation(commands.Cog): + """Moderation commands for server management""" + + def __init__(self, bot): + self.bot = bot + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='setrole', aliases=['giverole'], help='sets an existing role which are below icy404(role) for a user', pass_context=True, description=',setrole ') + @commands.has_permissions(administrator=True) + async def setrole_command(self, ctx, user: nextcord.Member, role: nextcord.Role): + """Assign a role to a user""" + await user.add_roles(role) + embed = nextcord.Embed(description=f"`{user.name}` has been given a role called: **{role.name}**", color=self.bot.embed_color) + await ctx.send(embed=embed) + + @commands.command() + @has_permissions(kick_members=True, administrator=True) + async def kick(self, ctx, member: discord.Member, *, reason=None): + """Kick a member from the server""" + guild = ctx.guild + memberKick = discord.Embed(title='Kicked', description=f'You have been kicked from {guild.name} for {reason}') + + await member.kick(reason=reason) + await ctx.send(f'User {member} has been kicked.') + + @commands.command() + @has_permissions(ban_members=True, administrator=True) + async def ban(self, ctx, member: discord.Member, *, reason=None): + """Ban a member from the server""" + guild = ctx.guild + memberBan = discord.Embed(title='Banned', description=f'You were banned from {guild.name} for {reason}') + + await member.ban(reason=reason) + await ctx.send(f'User {member} has been banned.') + try: + await member.send(embed=memberBan) + except: + pass # User may have DMs disabled + + @commands.command() + @has_permissions(ban_members=True, administrator=True) + async def unban(self, ctx, *, member): + """Unban a member from the server""" + banned_users = await ctx.guild.bans() + member_name, member_discriminator = member.split('#') + + for ban_entry in banned_users: + user = ban_entry.user + if (user.name, user.discriminator) == (member_name, member_discriminator): + await ctx.guild.unban(user) + await ctx.send(f'{user.name}#{user.discriminator} has been unbanned.') + return + + await ctx.send(f"User {member} not found in ban list.") + + @commands.command(pass_context=True) + @has_permissions(manage_messages=True) + async def mute(self, ctx, member: discord.Member, reason=None): + """Mute a member""" + guild = ctx.guild + mutedRole = discord.utils.get(guild.roles, name='Muted') + memberMute = discord.Embed(title='Muted', description=f'You have been muted from {guild.name} for {reason}') + + if mutedRole not in guild.roles: + perms = discord.Permissions(send_messages=False, speak=False) + mutedRole = await guild.create_role(name='Muted', permissions=perms) + await member.add_roles(mutedRole) + await ctx.send('Successfully created the [Muted] role and properly assigned it to the user.') + else: + await member.add_roles(mutedRole) + + embed = discord.Embed(title='User muted!', description=f'**{member}** was muted by **{ctx.message.author}**!', color=0xff00f6) + await ctx.send(embed=embed) + + @commands.command(pass_context=True) + @has_permissions(manage_messages=True) + async def unmute(self, ctx, member: discord.Member, *, reason=None): + """Unmute a member""" + guild = ctx.guild + mutedRole = discord.utils.get(guild.roles, name='Muted') + + memberUnmute = discord.Embed(title='Unmuted', description=f'You were unmuted from {guild.name} for {reason}') + + await member.remove_roles(mutedRole) + await ctx.send(f'Unmuted {member.mention} for {reason}') + try: + await member.send(embed=memberUnmute) + except: + pass # User may have DMs disabled + + +async def setup(bot): + """Load the Moderation cog""" + await bot.add_cog(Moderation(bot)) diff --git a/cogs/music.py b/cogs/music.py new file mode 100644 index 0000000..9a1b899 --- /dev/null +++ b/cogs/music.py @@ -0,0 +1,477 @@ +""" +Music cog for iceC Discord Bot +Handles all music playback, queue management, and playback controls +""" +import os +import datetime +import random +import numpy as np +import logging +from typing import Optional +import nextcord +from nextcord.ext import commands +import wavelink +from wavelink.ext import spotify + +logger = logging.getLogger('discord') + + +# Helper function for voice connectivity check +async def user_connectivity(ctx: commands.Context): + """Check if user is connected to a voice channel""" + if not getattr(ctx.author.voice, 'channel', None): + await ctx.send(embed=nextcord.Embed(description=f'Try after joining a `voice channel`', color=ctx.bot.embed_color)) + return False + return True + + +class Music(commands.Cog): + """Music playback and queue management commands""" + + def __init__(self, bot): + self.bot = bot + setattr(wavelink.Player, 'lq', False) + + @commands.Cog.listener() + async def on_wavelink_node_ready(self, node: wavelink.Node): + """Event fired when a node has finished connecting""" + print(f'Node {node.identifier} connected successfully') + + @commands.Cog.listener() + async def on_wavelink_track_end(self, player: wavelink.Player, track: wavelink.Track, reason): + """Event fired when a track has finished playing""" + ctx = player.ctx + vc: wavelink.Player = ctx.voice_client + + if vc.loop: + return await vc.play(track) + + try: + if not vc.queue.is_empty: + if vc.lq: + vc.queue.put(vc.queue._queue[0]) + next_song = vc.queue.get() + await vc.play(next_song) + await ctx.send(embed=nextcord.Embed(description=f'**Current song playing from the `QUEUE`**\\n\\n`{next_song.title}`', color=ctx.bot.embed_color), delete_after=30) + except Exception as e: + logger.error(f"Error in on_wavelink_track_end: {e}") + await vc.stop() + return await ctx.send(embed=nextcord.Embed(description=f'No songs in the `QUEUE`', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 1, commands.BucketType.user) + @commands.command(name='play', aliases=['p'], help='plays the given track provided by the user', description=',p ') + async def play_command(self, ctx: commands.Context, *, search: wavelink.YouTubeTrack): + """Play a song from YouTube""" + if not getattr(ctx.author.voice, 'channel', None): + return await ctx.send(embed=nextcord.Embed(description=f'Try after joining voice channel', color=ctx.bot.embed_color)) + elif not ctx.voice_client: + vc: wavelink.Player = await ctx.author.voice.channel.connect(cls=wavelink.Player) + else: + vc: wavelink.Player = ctx.voice_client + + if vc.queue.is_empty and vc.is_playing() is False: + playString = await ctx.send(embed=nextcord.Embed(description='**searching...**', color=ctx.bot.embed_color)) + await vc.play(search) + await playString.edit(embed=nextcord.Embed(description=f'**Search found**\\n\\n`{search.title}`', color=ctx.bot.embed_color)) + else: + await vc.queue.put_wait(search) + await ctx.send(embed=nextcord.Embed(description=f'Added to the `QUEUE`\\n\\n`{search.title}`', color=ctx.bot.embed_color)) + + vc.ctx = ctx + setattr(vc, 'loop', False) + self.bot.user_dict[search.identifier] = ctx.author.mention + + @commands.cooldown(1, 1, commands.BucketType.user) + @commands.command(name='splay', aliases=['sp'], help='plays the provided spotify playlist link', description=',sp ') + async def spotifyplay_command(self, ctx: commands.Context, search: str): + """Play a Spotify playlist""" + if not getattr(ctx.author.voice, 'channel', None): + return await ctx.send(embed=nextcord.Embed(description=f'Try after joining voice channel', color=ctx.bot.embed_color)) + elif not ctx.voice_client: + vc: wavelink.Player = await ctx.author.voice.channel.connect(cls=wavelink.Player) + else: + vc: wavelink.Player = ctx.voice_client + + async for partial in spotify.SpotifyTrack.iterator(query=search, type=spotify.SpotifySearchType.playlist, partial_tracks=True): + if vc.queue.is_empty and vc.is_playing() is False: + await vc.play(partial) + else: + await vc.queue.put_wait(partial) + song_name = await wavelink.tracks.YouTubeTrack.search(partial.title) + self.bot.user_dict[song_name[0].identifier] = ctx.author.mention + + vc.ctx = ctx + setattr(vc, 'loop', False) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='pause', aliases=['stop'], help='pauses the current playing track', description=',pause') + async def pause_command(self, ctx: commands.Context): + """Pause the current track""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc._source: + if not vc.is_paused(): + await vc.pause() + await ctx.send(embed=nextcord.Embed(description='`PAUSED` the music!', color=ctx.bot.embed_color)) + elif vc.is_paused(): + await ctx.send(embed=nextcord.Embed(description='Already in `PAUSED State`', color=ctx.bot.embed_color)) + elif not vc._source: + await ctx.send(embed=nextcord.Embed(description='Player is not `playing`!', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='resume', aliases=[], help='resumes the paused track', description=',resume') + async def resume_command(self, ctx: commands.Context): + """Resume the paused track""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc.is_paused(): + await vc.resume() + await ctx.send(embed=nextcord.Embed(description='Music `RESUMED`!', color=ctx.bot.embed_color)) + elif vc.is_playing(): + await ctx.send(embed=nextcord.Embed(description='Already in `RESUMED State`', color=ctx.bot.embed_color)) + else: + await ctx.send(embed=nextcord.Embed(description='Player is not `playing`!', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='skip', aliases=['next', 's'], help='skips to the next track', description=',s') + @commands.has_role('tm') + async def skip_command(self, ctx: commands.Context): + """Skip to the next track""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc.loop == True: + vclooptxt = 'Disable the `LOOP` to skip | **,loop** again to disable the `LOOP` | Add a new song to disable the `LOOP`' + return await ctx.send(embed=nextcord.Embed(description=vclooptxt, color=ctx.bot.embed_color)) + elif vc.queue.is_empty: + await vc.stop() + await vc.resume() + return await ctx.send(embed=nextcord.Embed(description=f'Song stopped! No songs in the `QUEUE`', color=ctx.bot.embed_color)) + else: + await vc.stop() + vc.queue._wakeup_next() + await vc.resume() + return await ctx.send(embed=nextcord.Embed(description=f'`SKIPPED`!', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='disconnect', aliases=['dc', 'leave'], help='disconnects the player from the vc', description=',dc') + @commands.has_role('tm') + async def disconnect_command(self, ctx: commands.Context): + """Disconnect from voice channel""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + try: + await vc.disconnect(force=True) + await ctx.send(embed=nextcord.Embed(description='**BYE!** Have a great time!', color=ctx.bot.embed_color)) + except Exception as e: + logger.error(f"Error disconnecting voice client: {e}") + await ctx.send(embed=nextcord.Embed(description='Failed to disconnect!', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='nowplaying', aliases=['np'], help='shows the current track information', description=',np') + async def nowplaying_command(self, ctx: commands.Context): + """Show current track information""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if not vc.is_playing(): + return await ctx.send(embed=nextcord.Embed(description='Not playing anything!', color=ctx.bot.embed_color)) + + if vc.loop: + loopstr = 'enabled' + else: + loopstr = 'disabled' + + if vc.is_paused(): + state = 'paused' + else: + state = 'playing' + + '''numpy array usertag indexing''' + user_list = list(self.bot.user_dict.items()) + user_arr = np.array(user_list) + song_index = np.flatnonzero(np.core.defchararray.find(user_arr, vc.track.identifier) == 0) + if len(song_index) > 0: + arr_index = int(song_index[0]/2) + requester = user_arr[arr_index, 1] if arr_index < len(user_arr) else "Unknown" + else: + requester = "Unknown" + + nowplaying_description = f'[`{vc.track.title}`]({str(vc.track.uri)})\\n\\n**Requested by**: {requester}' + em = nextcord.Embed(description=f'**Now Playing**\\n\\n{nowplaying_description}', color=ctx.bot.embed_color) + em.add_field(name='**Song Info**', value=f'• Author: `{vc.track.author}`\\n• Duration: `{str(datetime.timedelta(seconds=vc.track.length))}`') + em.add_field(name='**Player Info**', value=f'• Player Volume: `{vc._volume}`\\n• Loop: `{loopstr}`\\n• Current State: `{state}`', inline=False) + return await ctx.send(embed=em) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='loop', aliases=[], help='•loops the current song\\n•unloops the current song', description=',loop') + @commands.has_role('tm') + async def loop_command(self, ctx: commands.Context): + """Loop/unloop the current song""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc._source: + try: + vc.loop ^= True + except Exception as e: + logger.error(f"Error toggling loop: {e}") + setattr(vc, 'loop', False) + else: + return await ctx.send(embed=nextcord.Embed(description='No song to `loop`', color=ctx.bot.embed_color)) + if vc.loop: + return await ctx.send(embed=nextcord.Embed(description='**LOOP**: `enabled`', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description='**LOOP**: `disabled`', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='loopqueue', aliases=['lq'], help='starts the loop queue ==> ,lq start or ,lq enable\\nstopes the loop queue ==> ,lq stop or ,lq disable', description=',lq ') + @commands.has_role('tm') + async def loopqueue_command(self, ctx: commands.Context, type: str): + """Enable/disable queue looping""" + vc: wavelink.Player = ctx.voice_client + if not vc.queue.is_empty: + song_count = len(vc.queue) + if vc.lq == False: + if type == 'start' or type == 'enable': + vc.lq = True + await ctx.send(embed=nextcord.Embed(description='**loopqueue**: `enabled`', color=ctx.bot.embed_color)) + try: + if vc._source and vc._source not in vc.queue: + vc.queue.put(vc._source) + except Exception as e: + logger.error(f"Error adding source to queue: {e}") + return + if vc.lq == True: + if type == 'stop' or type == 'disable': + vc.lq = False + await ctx.send(embed=nextcord.Embed(description='**loopqueue**: `disabled`', color=ctx.bot.embed_color)) + if song_count == 1 and vc.queue._queue and len(vc.queue._queue) > 0 and vc.queue._queue[0] == vc._source: + del vc.queue._queue[0] + if type != 'start' and type != 'enable' and type != 'disable' and type != 'stop': + await ctx.send(embed=nextcord.Embed(description='check **,help** for **loopqueue**', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description='Unable to loop `QUEUE`, try adding more songs..', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='queue', aliases=['q', 'track'], help='displays the current queue', description=',q') + async def queue_command(self, ctx: commands.Context): + """Display the current queue""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc.queue.is_empty: + return await ctx.send(embed=nextcord.Embed(description='**QUEUE**\\n\\n`empty`', color=ctx.bot.embed_color)) + + lqstr = '`disabled`' if vc.lq == False else '`enabled`' + if not hasattr(self.bot, 'qem'): + self.bot.qem = None + qem = nextcord.Embed(description=f'**QUEUE**\\n\\n**loopqueue**: {lqstr}', color=ctx.bot.embed_color) + song_queue = vc.queue.copy() + song_count = 0 + for song in song_queue: + song_count += 1 + if wavelink.tracks.PartialTrack: + title = song.title + else: + title = song.info['title'] + qem.add_field(name=f'‎', value=f'**{song_count} **• {title}', inline=False) + self.bot.qem = qem + + await ctx.send(embed=qem) + return commands.Paginator(prefix='>', suffix='<', linesep='\\n') + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name="shuffle", aliases=['mix'], help='shuffles the existing queue randomly', description=',shuffle') + @commands.has_role('tm') + async def shuffle_command(self, ctx: commands.Context): + """Shuffle the queue""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + song_count = len(vc.queue) + if song_count > 2: + random.shuffle(vc.queue._queue) + return await ctx.send(embed=nextcord.Embed(description=f'Shuffled the `QUEUE`', color=ctx.bot.embed_color)) + elif vc.queue.is_empty: + return await ctx.send(embed=nextcord.Embed(description=f'`QUEUE` is empty', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description=f'`QUEUE` has less than `3 songs`', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='del', aliases=['remove', 'drop'], help='deletes the specified track', description=',del ') + @commands.has_role('tm') + async def del_command(self, ctx: commands.Context, position: int): + """Delete a track from the queue""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if not vc.queue.is_empty: + song_count = len(vc.queue) + if position <= 0: + return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=ctx.bot.embed_color)) + elif position > song_count: + return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range', color=ctx.bot.embed_color)) + else: + SongToBeDeleted = vc.queue._queue[position-1].title + del vc.queue._queue[position-1] + return await ctx.send(embed=nextcord.Embed(description=f'`{SongToBeDeleted}` removed from the QUEUE', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='skipto', aliases=['goto'], help='skips to the specified track', description=',skipto ') + @commands.has_role('tm') + async def skipto_command(self, ctx: commands.Context, position: int): + """Skip to a specific track in the queue""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if not vc.queue.is_empty: + song_count = len(vc.queue) + if position <= 0: + return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=ctx.bot.embed_color)) + elif position > song_count: + return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range', color=ctx.bot.embed_color)) + else: + vc.queue.put_at_front(vc.queue._queue[position-1]) + del vc.queue._queue[position] + return await self.skip_command(ctx) + else: + return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='move', aliases=['set'], help='moves the track to the specified position', description=',move ') + @commands.has_role('tm') + async def move_command(self, ctx: commands.Context, song_position: int, move_position: int): + """Move a track to a different position in the queue""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if not vc.queue.is_empty: + song_count = len(vc.queue) + if song_position <= 0 or move_position <= 0: + return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=ctx.bot.embed_color)) + elif song_position > song_count or move_position > song_count: + position = song_position if song_position > song_count else move_position + return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range!', color=ctx.bot.embed_color)) + elif song_position == move_position: + return await ctx.send(embed=nextcord.Embed(description=f'Already in that `Position`:{move_position}', color=ctx.bot.embed_color)) + else: + move_index = move_position-1 if move_position < song_position else move_position + song_index = song_position if move_position < song_position else song_position-1 + vc.queue.put_at_index(move_index, vc.queue._queue[song_position-1]) + moved_song = vc.queue._queue[song_index] + del vc.queue._queue[song_index] + moved_song_name = moved_song.info['title'] + return await ctx.send(embed=nextcord.Embed(description=f'**{moved_song_name}** moved at Position:`{move_position}`', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`!', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='volume', aliases=['vol'], help='sets the volume', description=',vol ') + @commands.has_role('tm') + async def volume_command(self, ctx: commands.Context, playervolume: int): + """Set the player volume""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if vc.is_connected(): + if playervolume > 100: + return await ctx.send(embed=nextcord.Embed(description='**VOLUME** supported upto `100%`', color=ctx.bot.embed_color)) + elif playervolume < 0: + return await ctx.send(embed=nextcord.Embed(description='**VOLUME** can not be `negative`', color=ctx.bot.embed_color)) + else: + await ctx.send(embed=nextcord.Embed(description=f'**VOLUME**\\nSet to `{playervolume}%`', color=ctx.bot.embed_color)) + return await vc.set_volume(playervolume) + elif not vc.is_connected(): + return await ctx.send(embed=nextcord.Embed(description="Player not connected!", color=ctx.bot.embed_color)) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='seek', aliases=[], help='seeks or moves the player to specified track position', description=',seek ') + @commands.has_role('tm') + async def seek_command(self, ctx: commands.Context, seekPosition: int): + """Seek to a position in the track""" + if await user_connectivity(ctx) == False: + return + else: + vc: wavelink.Player = ctx.voice_client + if not vc.is_playing(): + return await ctx.send(embed=nextcord.Embed(description='Player not playing!', color=ctx.bot.embed_color)) + elif vc.is_playing(): + if 0 <= seekPosition <= vc.track.length: + msg = await ctx.send(embed=nextcord.Embed(description='seeking...', color=ctx.bot.embed_color)) + await vc.seek(seekPosition*1000) + return await msg.edit(embed=nextcord.Embed(description=f'Player SEEKED: `{seekPosition}` seconds', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description=f'SEEK length `{seekPosition}` outta range', color=ctx.bot.embed_color)) + + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.command(name='clear', aliases=[], help='clears the queue', description=',clear') + @commands.has_role('tm') + async def clear_command(self, ctx: commands.Context): + """Clear the entire queue""" + vc: wavelink.Player = ctx.voice_client + if await user_connectivity(ctx) == False: + return + else: + if vc.queue.is_empty: + return await ctx.send(embed=nextcord.Embed(description='No `SONGS` are present', color=ctx.bot.embed_color)) + else: + vc.queue._queue.clear() + vc.lq = False + clear_command_embed = nextcord.Embed(description=f'`QUEUE` cleared', color=ctx.bot.embed_color) + return await ctx.send(embed=clear_command_embed) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='save', aliases=['dm'], description=",save\n,save ", help='dms the current | specified song to the user') + async def save_command(self, ctx: commands.Context, savestr: Optional[str]): + """DM the current song or queue to the user""" + vc: wavelink.Player = ctx.voice_client + if await user_connectivity(ctx) == False: + return + else: + user = await self.bot.fetch_user(ctx.author._user.id) + if vc._source and savestr is None: + await user.send(embed=nextcord.Embed(description=f'`{vc._source}`', color=ctx.bot.embed_color)) + elif not vc.queue.is_empty and (savestr == 'q' or savestr == 'queue'): + if hasattr(self.bot, 'qem') and self.bot.qem: + await user.send(embed=self.bot.qem) + else: + await ctx.send(embed=nextcord.Embed(description='Queue not available. Please use ,q first.', color=ctx.bot.embed_color)) + elif not vc.queue.is_empty and savestr: + song_count = len(vc.queue) + try: + position = int(savestr) + if position <= 0: + return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=ctx.bot.embed_color)) + elif position > song_count: + return await ctx.send(embed=nextcord.Embed(description=f'Position `{savestr}` is outta range', color=ctx.bot.embed_color)) + else: + song_info = vc.queue._queue[position - 1] + em = nextcord.Embed(description=song_info.info['title'], color=ctx.bot.embed_color) + await user.send(embed=em) + except ValueError: + return await ctx.send(embed=nextcord.Embed(description='Invalid position. Please provide a number.', color=ctx.bot.embed_color)) + else: + return await ctx.send(embed=nextcord.Embed(description='There is no `song` | `queue` available', color=ctx.bot.embed_color)) + + +async def setup(bot): + """Load the Music cog""" + await bot.add_cog(Music(bot)) diff --git a/cogs/trading.py b/cogs/trading.py new file mode 100644 index 0000000..9ad1140 --- /dev/null +++ b/cogs/trading.py @@ -0,0 +1,167 @@ +""" +Trading cog for iceC Discord Bot +Handles Binance trading features including favorites and position alerts +""" +import os +import json +from discord.ext import commands, tasks +from binance import Client + + +class Trading(commands.Cog): + """Binance trading features and commands""" + + def __init__(self, bot): + self.bot = bot + self.binance_client = None + self.channel_id = None + self.fav_list = {} + self._initialize_binance() + + def _initialize_binance(self): + """Initialize Binance client and load favorites""" + binance_api_key = os.getenv('BINANCE_API_KEY') + binance_api_secret = os.getenv('BINANCE_API_SECRET') + self.channel_id = os.getenv('CHANNEL_ID') + + if binance_api_key and binance_api_secret: + try: + self.binance_client = Client(binance_api_key, binance_api_secret) + # Load favorites list + try: + with open('FAV_LIST.json') as f: + self.fav_list = json.load(f) + except FileNotFoundError: + self.fav_list = {"FUTURES": {}, "SPOT": {}} + except Exception as e: + print(f"Failed to initialize Binance client: {e}") + self.binance_client = None + + def get_future_position(self, symbol): + """Get future position for a symbol""" + if not self.binance_client: + return None + try: + position = None + positions = list(filter(lambda f: (f['symbol'] == symbol), self.binance_client.futures_account()['positions'])) + if positions: + position = positions[0] + return position + except Exception as e: + print(f"Error getting future position: {e}") + return None + + @commands.command() + async def add_fav(self, ctx, account, symbol): + """Add a symbol to favorites list""" + if not self.binance_client: + return await ctx.send("Binance API not configured. Please set BINANCE_API_KEY and BINANCE_API_SECRET in .env") + + try: + FUT_SYMBOLS = [sym['symbol'] for sym in self.binance_client.futures_exchange_info()['symbols']] + SPOT_SYMBOLS = [sym['symbol'] for sym in self.binance_client.get_all_tickers()] + + if account.upper() == "FUT": + if symbol in FUT_SYMBOLS: + self.fav_list['FUTURES'][symbol] = {} + await ctx.send(f"Added {symbol} to FUTURES favorites") + else: + await ctx.send("Provided SYMBOL or CRYPTO is not available in Futures") + elif account.upper() == "SPOT": + if symbol in SPOT_SYMBOLS: + self.fav_list['SPOT'][symbol] = {} + await ctx.send(f"Added {symbol} to SPOT favorites") + else: + await ctx.send("Provided SYMBOL or CRYPTO is not available in SPOT") + else: + await ctx.send('Provided Account Type is not valid. Please use FUT for Futures and SPOT for spot') + + # Save favorites + with open('FAV_LIST.json', 'w') as f: + json.dump(self.fav_list, f) + except Exception as e: + await ctx.send(f"Error adding favorite: {e}") + + @commands.command() + async def favs(self, ctx): + """List favorite cryptocurrencies""" + if not self.binance_client: + return await ctx.send("Binance API not configured.") + + try: + message = "FUTURES FAVOURITE LIST\n" + for i, symbol in enumerate(self.fav_list['FUTURES'].keys()): + ticker = self.binance_client.get_ticker(symbol=symbol) + message += str(i+1) + ". " + symbol + "--> Last Price: " + ticker['lastPrice'] + "\n" + message += "\n\nSPOT FAVOURITE LIST" + for i, symbol in enumerate(self.fav_list['SPOT'].keys()): + ticker = self.binance_client.get_ticker(symbol=symbol) + message += str(i+1) + ". " + symbol + "--> Last Price: " + ticker['lastPrice'] + "\n" + await ctx.send(message) + except Exception as e: + await ctx.send(f"Error fetching favorites: {e}") + + @commands.command() + async def fubln(self, ctx): + """Show futures account balance""" + if not self.binance_client: + return await ctx.send("Binance API not configured.") + + try: + balance_list = self.binance_client.futures_account_balance() + message = "-"*35 + "\n" + message += "-"*3 + "ACCOUNT BALANCE" + "-"*3 + "\n" + message += "-"*35 + "\n" + for balance in balance_list: + message += balance['asset'] + " : " + balance['balance'] + "\n" + message += "-"*35 + await ctx.send(message) + except Exception as e: + await ctx.send(f"Error fetching balance: {e}") + + @tasks.loop(seconds=60) + async def futures_position_alerts(self): + """Automated task to monitor futures positions and send alerts""" + if not self.binance_client or not self.channel_id: + return + + try: + futures_info = self.binance_client.futures_account() + positions_info = self.binance_client.futures_position_information() + positions = futures_info['positions'] + message_channel = await self.bot.fetch_channel(self.channel_id) + print(f"Got channel {message_channel} for {self.channel_id}") + + if float(futures_info['totalMaintMargin'])/float(futures_info['totalMarginBalance']) > 40.0: + await message_channel.send("Your positions' Margin Ratio is greater than 40%. Please consider taking a look at it.") + + for position in positions: + symbol = position['symbol'] + alert = False + message = "------" + symbol + " POSITION ALERT!------\n" + position_info = list(filter(lambda f: (f['symbol'] == symbol), positions_info))[0] + if float(position_info['positionAmt']) != 0.0: + if float(position['unrealizedProfit']) < -1.0: + message += "Unrealized Profit is going down! LOSS : " + str(position['unrealizedProfit']) + "\n" + alert = True + if (float(position_info['markPrice'])-float(position_info['liquidationPrice']))/(float(position_info['entryPrice'])-float(position_info['liquidationPrice'])) <= 0.4: + message += "Mark price is moving closer to Liquidation Price. Your position may be liquidated soon.\n Mark Price:" + str(position_info['markPrice']) + "\n Liquidation Price:" + str(position_info['liquidationPrice']) + "\n" + alert = True + if alert: + await message_channel.send(message) + except Exception as e: + print(f"Error in futures_position_alerts: {e}") + + @futures_position_alerts.before_loop + async def before_futures_alerts(self): + """Wait until bot is ready before starting alerts""" + await self.bot.wait_until_ready() + print("Finished waiting for futures alerts") + + +async def setup(bot): + """Load the Trading cog""" + trading = Trading(bot) + await bot.add_cog(trading) + # Uncomment the line below to enable position alerts + # trading.futures_position_alerts.start() diff --git a/cogs/utility.py b/cogs/utility.py new file mode 100644 index 0000000..397aba5 --- /dev/null +++ b/cogs/utility.py @@ -0,0 +1,41 @@ +""" +Utility cog for iceC Discord Bot +Handles utility commands like ping, info, etc. +""" +import nextcord +from nextcord.ext import commands + + +class Utility(commands.Cog): + """Utility commands""" + + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message): + """Handle simple message responses""" + if message.author == self.bot.user: + return + if message.content.startswith('hello'): + await message.channel.send('Hello!') + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='ping', help=f"displays client's latency", description=',ping') + async def ping_command(self, ctx): + """Check bot latency""" + em = nextcord.Embed(description=f'**Pong!**\\n\\n`{round(self.bot.latency*1000)}`ms', color=self.bot.embed_color) + await ctx.send(embed=em) + + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.command(name='info', aliases=['i'], help='shows information about the client') + @commands.is_owner() + @commands.has_role('tm') + async def info_command(self, ctx: commands.Context): + """Show bot information (Owner only)""" + await ctx.send(embed=nextcord.Embed(description=f'**Info**\\ntotal server count: `{len(self.bot.guilds)}`', color=self.bot.embed_color)) + + +async def setup(bot): + """Load the Utility cog""" + await bot.add_cog(Utility(bot)) diff --git a/main.py b/main.py index 35521ef..518deb1 100644 --- a/main.py +++ b/main.py @@ -1,741 +1,153 @@ -import os, json +""" +iceC Discord Bot - Main File +A feature-rich Discord bot with music, trading, and moderation features +""" +import os +import json from dotenv import load_dotenv, find_dotenv import discord -from discord.ext import commands, tasks +from discord.ext import commands import logging -import binance -from discord import Member -from discord.ext.commands import has_permissions, MissingPermissions -from binance import Client, ThreadedWebsocketManager, ThreadedDepthCacheManager -import datetime, random +import numpy as np import nextcord -from nextcord.ext import commands import wavelink from wavelink.ext import spotify -from typing import Optional -import numpy as np -import lyricsgenius +# Setup logging logger = logging.getLogger('discord') logger.setLevel(logging.DEBUG) -handler = logging.FileHandler(filename = 'discord.log', encoding='utf-8', mode='w') -handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:&(message)s')) +handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') +handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(message)s')) logger.addHandler(handler) - +# Load environment variables load_dotenv(find_dotenv()) token = os.getenv("DISCORD_TOKEN") -channel_id = os.getenv('CHANNEL_ID') -binance_api_key = os.getenv('BINANCE_API_KEY') -binance_api_secret = os.getenv('BINANCE_API_SECRET') +# Setup Discord intents intents = discord.Intents.default() intents.guild_messages = True intents.members = True intents.message_content = True intents.voice_states = True intents.emojis_and_stickers = True -all_intents = intents.all() -all_intents= True -intent = discord.Intents.default() - -client = discord.Client(command_prefix='!', intents=intents, case_insensitive=True) -global user_arr, user_dict -user_dict = {} -user_arr = np.array([]) -setattr(wavelink.Player, 'lq', False) -embed_color = nextcord.Color.from_rgb(128, 67, 255) - -binanceClient = Client(binance_api_key, binance_api_secret) +# Create bot instance +bot = commands.Bot(command_prefix='!', intents=intents, case_insensitive=True) +client = bot # Alias for backwards compatibility +# Initialize bot attributes +bot.user_dict = {} +bot.user_arr = np.array([]) +bot.embed_color = nextcord.Color.from_rgb(128, 67, 255) +bot.qem = None # For storing queue embed -FAV_LIST = {} -with open('FAV_LIST.json') as f: - FAV_LIST = json.load(f) - -def get_future_position(symbol): - position = None - positions = list(filter(lambda f:(f['symbol']==symbol), binanceClient.futures_account()['positions'])) - if positions: - position = positions[0] - return position +# Setup Wavelink Player attribute +setattr(wavelink.Player, 'lq', False) -@client.event +async def node_connect(): + """Connect to Lavalink node for music playback""" + await bot.wait_until_ready() + try: + lavalink_host = os.getenv('LAVALINK_HOST', 'node1.kartadharta.xyz') + lavalink_port = int(os.getenv('LAVALINK_PORT', '443')) + lavalink_password = os.getenv('LAVALINK_PASSWORD') + + if not lavalink_password: + logger.warning("LAVALINK_PASSWORD not set in environment variables. Music features may not work.") + return + + await wavelink.NodePool.create_node( + client=bot, + host=lavalink_host, + port=lavalink_port, + password=lavalink_password, + https=True, + spotify_client=spotify.SpotifyClient( + client_id=os.environ.get('spotify_id'), + client_secret=os.environ.get('spotify_secret') + ) + ) + except Exception as e: + logger.error(f"Failed to connect to Lavalink node: {e}") + + +@bot.event async def on_ready(): - print(f'We have logged in as {client.user}') - - -@client.event -async def on_message(message): - if message.author == client.user: - return - if message.content.startswith('hello'): - await message.channel.send('Hello!') - - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='setrole', aliases=['giverole'],help='sets an existing role which are below icy404(role) for a user', pass_context=True, description=',setrole ') -@commands.has_permissions(administrator=True) -async def setrole_command(ctx, user: nextcord.Member, role: nextcord.Role): - await user.add_roles(role) - embed = nextcord.Embed(description=f"`{user.name}` has been given a role called: **{role.name}**", color=embed_color) - await ctx.send(embed=embed) + """Event fired when bot is ready""" + print(f'We have logged in as {bot.user}') + print(f'Bot is in {len(bot.guilds)} guilds') -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='ping', help=f"displays client's latency", description=',ping') -async def ping_command(ctx): - em = nextcord.Embed(description=f'**Pong!**\n\n`{round(client.latency*1000)}`ms', color=embed_color) - await ctx.send(embed=em) - -async def user_connectivity(ctx: commands.Context): - # vc: wavelink.Player = ctx.voice_client - if not getattr(ctx.author.voice, 'channel', None): - await ctx.send(embed=nextcord.Embed(description=f'Try after joining a `voice channel`', color=embed_color)) - return False - #-->code to check if the client is connected to vc??<-- - -@client.event -async def on_wavelink_node_ready(node: wavelink.Node): - print(f'Node {node.identifier} connected successfully') - -async def node_connect(): - await client.wait_until_ready() - await wavelink.NodePool.create_node(client=client, host='node1.kartadharta.xyz', port=443, password="kdlavalink", https=True, spotify_client=spotify.SpotifyClient(client_id=os.environ['spotify_id'],client_secret=os.environ['spotify_secret'])) -@client.event -async def on_wavelink_track_end(player: wavelink.Player, track: wavelink.Track, reason): - ctx = player.ctx - vc: player = ctx.voice_client + # Connect to Lavalink + await node_connect() - if vc.loop: - return await vc.play(track) + print('Bot is ready!') - try: - if not vc.queue.is_empty: - if vc.lq: - vc.queue.put(vc.queue._queue[0]) - next_song = vc.queue.get() - await vc.play(next_song) - await ctx.send(embed=nextcord.Embed(description=f'**Current song playing from the `QUEUE`**\n\n`{next_song.title}`', color=embed_color), delete_after=30) - #{code to remove the song name from the numpy array} - except: - await vc.stop() - return await ctx.send(embed=nextcord.Embed(description=f'No songs in the `QUEUE`', color=embed_color)) -@client.event +@bot.event async def on_command_error(ctx: commands.Context, error): + """Global error handler for commands""" if isinstance(error, commands.CommandOnCooldown): - em = nextcord.Embed(description=f'**Cooldown active**\ntry again in `{error.retry_after:.2f}`s*',color=embed_color) + em = nextcord.Embed( + description=f'**Cooldown active**\ntry again in `{error.retry_after:.2f}`s*', + color=bot.embed_color + ) await ctx.send(embed=em) - -@client.event -async def on_command_error(ctx: commands.Context, error): - if isinstance(error, commands.MissingRequiredArgument): - await ctx.send(embed=nextcord.Embed(description="Missing `arguments`", color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='info',aliases=['i'], help='shows information about the client') -@commands.is_owner() -@commands.has_role('tm') -async def info_command(ctx: commands.Context): - await ctx.send(embed=nextcord.Embed(description=f'**Info**\ntotal server count: `{len(client.guilds)}`', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='loopqueue', aliases=['lq'], help='starts the loop queue ==> ,lq start or ,lq enable\nstopes the loop queue ==> ,lq stop or ,lq disable', description=',lq ') -@commands.has_role('tm') -async def loopqueue_command(ctx: commands.Context, type:str): - vc: wavelink.Player = ctx.voice_client - if not vc.queue.is_empty: - if vc.lq == False: - if type == 'start' or type == 'enable': - vc.lq = True - await ctx.send(embed=nextcord.Embed(description='**loopqueue**: `enabled`', color=embed_color)) - try: - if vc._source not in vc.queue: - vc.queue.put(vc._source) - else: '' - except Exception: - return '' - if vc.lq == True: - if type == 'stop' or type == 'disable': - vc.lq = False - await ctx.send(embed=nextcord.Embed(description='**loopqueue**: `disabled`', color=embed_color)) - if song_count == 1 and vc.queue._queue[0] == vc._source: - del vc.queue._queue[0] - else: - return '' - if type != 'start' and type != 'enable' and type != 'disable' and type != 'stop': - await ctx.send(embed=nextcord.Embed(description='check **,help** for **loopqueue**', color=embed_color)) + elif isinstance(error, commands.MissingRequiredArgument): + await ctx.send(embed=nextcord.Embed(description="Missing `arguments`", color=bot.embed_color)) + elif isinstance(error, commands.CommandNotFound): + pass # Ignore unknown commands else: - return await ctx.send(embed=nextcord.Embed(description='Unable to loop `QUEUE`, try adding more songs..', color=embed_color)) + logger.error(f"Unhandled command error: {error}") -@commands.cooldown(1, 1, commands.BucketType.user) -@commands.command(name='play', aliases=['p'], help='plays the given track provided by the user', description=',p ') -async def play_command(ctx: commands.Context, *, search:wavelink.YouTubeTrack): - - if not getattr(ctx.author.voice, 'channel', None): - return await ctx.send(embed=nextcord.Embed(description=f'Try after joining voice channel', color=embed_color)) - elif not ctx.voice_client: - vc: wavelink.Player = await ctx.author.voice.channel.connect(cls=wavelink.Player) - else: - vc: wavelink.Player = ctx.voice_client - if vc.queue.is_empty and vc.is_playing() is False: - playString = await ctx.send(embed=nextcord.Embed(description='**searching...**', color=embed_color)) - await vc.play(search) - await playString.edit(embed=nextcord.Embed(description=f'**Search found**\n\n`{search.title}`', color=embed_color)) - - else: - await vc.queue.put_wait(search) - await ctx.send(embed=nextcord.Embed(description=f'Added to the `QUEUE`\n\n`{search.title}`', color=embed_color)) - - vc.ctx = ctx - - setattr(vc, 'loop', False) - - user_dict[search.identifier] = ctx.author.mention - -@commands.cooldown(1, 1, commands.BucketType.user) -@commands.command(name='splay', aliases=['sp'], help='plays the provided spotify playlist link', description=',sp ') -async def spotifyplay_command(ctx: commands.Context, search: str): - - if not getattr(ctx.author.voice, 'channel', None): - return await ctx.send(embed=nextcord.Embed(description=f'Try after joining voice channel', color=embed_color)) - elif not ctx.voice_client: - vc: wavelink.Player = await ctx.author.voice.channel.connect(cls=wavelink.Player) - else: - vc: wavelink.Player = ctx.voice_client +async def load_cogs(): + """Load all cogs""" + try: + await bot.load_extension('cogs.music') + print('✓ Loaded music cog') + except Exception as e: + print(f'✗ Failed to load music cog: {e}') - async for partial in spotify.SpotifyTrack.iterator(query=search, type=spotify.SpotifySearchType.playlist, partial_tracks=True): - if vc.queue.is_empty and vc.is_playing() is False: - await vc.play(partial) - else: - await vc.queue.put_wait(partial) - song_name = await wavelink.tracks.YouTubeTrack.search(partial.title) - user_dict[song_name[0].identifier] = ctx.author.mention - - vc.ctx = ctx + try: + await bot.load_extension('cogs.trading') + print('✓ Loaded trading cog') + except Exception as e: + print(f'✗ Failed to load trading cog: {e}') - setattr(vc, 'loop', False) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='pause', aliases=['stop'], help='pauses the current playing track', description=',pause') -async def pause_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - - if vc._source: - if not vc.is_paused(): - await vc.pause() - await ctx.send(embed=nextcord.Embed(description='`PAUSED` the music!', color=embed_color)) - - elif vc.is_paused(): - await ctx.send(embed=nextcord.Embed(description='Already in `PAUSED State`', color=embed_color)) - elif not vc._source: - await ctx.send(embed=nextcord.Embed(description='Player is not `playing`!', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='resume',aliases=[], help='resumes the paused track', description=',resume') -async def resume_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - - if vc.is_playing(): - if vc.is_paused(): - await vc.resume() - await ctx.send(embed=nextcord.Embed(description='Music `RESUMED`!', color=embed_color)) - - elif vc.is_playing(): - await ctx.send(embed=nextcord.Embed(description='Already in `RESUMED State`', color=embed_color)) - else: - await ctx.send(embed=nextcord.Embed(description='Player is not `playing`!', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='skip', aliases=['next', 's'], help='skips to the next track', description=',s') -@commands.has_role('tm') -async def skip_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - - if vc.loop == True: - vclooptxt = 'Disable the `LOOP` to skip | **,loop** again to disable the `LOOP` | Add a new song to disable the `LOOP`' - return await ctx.send(embed=nextcord.Embed(description=vclooptxt, color=embed_color)) - - elif vc.queue.is_empty: - await vc.stop() - await vc.resume() - return await ctx.send(embed=nextcord.Embed(description=f'Song stopped! No songs in the `QUEUE`', color=embed_color)) - - else: - await vc.stop() - vc.queue._wakeup_next() - await vc.resume() - return await ctx.send(embed=nextcord.Embed(description=f'`SKIPPED`!', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='disconnect', aliases=['dc', 'leave'], help='disconnects the player from the vc', description=',dc') -@commands.has_role('tm') -async def disconnect_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc : wavelink.Player = ctx.voice_client - try: - await vc.disconnect(force=True) - await ctx.send(embed=nextcord.Embed(description='**BYE!** Have a great time!', color=embed_color)) - except Exception: - await ctx.send(embed=nextcord.Embed(description='Failed to destroy!', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='nowplaying', aliases=['np'], help='shows the current track information', description=',np') -async def nowplaying_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if not vc.is_playing(): - return await ctx.send(embed=nextcord.Embed(description='Not playing anything!', color=embed_color)) - - #vcloop conditions - if vc.loop: - loopstr = 'enabled' - else: - loopstr = 'disabled' - - if vc.is_paused(): - state = 'paused' - else: - state = 'playing' - - '''numpy array usertag indexing''' - global user_list - user_list = list(user_dict.items()) - user_arr = np.array(user_list) - song_index = np.flatnonzero(np.core.defchararray.find(user_arr,vc.track.identifier) ==0) - arr_index = int(song_index/2) - - requester = user_arr[arr_index,1] - - nowplaying_description = f'[`{vc.track.title}`]({str(vc.track.uri)})\n\n**Requested by**: {requester}' - em = nextcord.Embed(description=f'**Now Playing**\n\n{nowplaying_description}', color=embed_color) - em.add_field(name='**Song Info**', value=f'• Author: `{vc.track.author}`\n• Duration: `{str(datetime.timedelta(seconds=vc.track.length))}`') - em.add_field(name='**Player Info**', value=f'• Player Volume: `{vc._volume}`\n• Loop: `{loopstr}`\n• Current State: `{state}`', inline=False) - - return await ctx.send(embed=em) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='loop',aliases=[], help='•loops the current song\n•unloops the current song', description=',loop') -@commands.has_role('tm') -async def loop_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if vc._source: - try: - vc.loop ^= True - except Exception: - setattr(vc, 'loop', False) - else: - return await ctx.send(embed= nextcord.Embed(description='No song to `loop`', color=embed_color)) - if vc.loop: - return await ctx.send(embed= nextcord.Embed(description='**LOOP**: `enabled`', color=embed_color)) - else: - return await ctx.send(embed=nextcord.Embed(description='**LOOP**: `disabled`', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='queue', aliases=['q', 'track'], help='displays the current queue', description=',q') -async def queue_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - - if vc.queue.is_empty: - return await ctx.send(embed= nextcord.Embed(description='**QUEUE**\n\n`empty`', color=embed_color)) - - lqstr = '`disabled`' if vc.lq == False else '`enabled`' - global qem - qem = nextcord.Embed(description=f'**QUEUE**\n\n**loopqueue**: {lqstr}',color=embed_color) - global song_count, song, song_queue - song_queue = vc.queue.copy() - song_count = 0 - for song in song_queue: - song_count += 1 - if wavelink.tracks.PartialTrack: - title = song.title - else: - title = song.info['title'] - qem.add_field(name=f'‎', value=f'**{song_count} **• {title}',inline=False) + try: + await bot.load_extension('cogs.moderation') + print('✓ Loaded moderation cog') + except Exception as e: + print(f'✗ Failed to load moderation cog: {e}') - await ctx.send(embed=qem) - return commands.Paginator(prefix='>', suffix='<', linesep='\n') - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name="shuffle", aliases=['mix'], help='shuffles the existing queue randomly', description=',shuffle') -@commands.has_role('tm') -async def shuffle_command(ctx: commands.Context): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if song_count > 2: - random.shuffle(vc.queue._queue) - return await ctx.send(embed=nextcord.Embed(description=f'Shuffled the `QUEUE`', color=embed_color)) - elif vc.queue.is_empty: - return await ctx.send(embed=nextcord.Embed(description=f'`QUEUE` is empty', color=embed_color)) - else: - return await ctx.send(embed=nextcord.Embed(description=f'`QUEUE` has less than `3 songs`', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='del', aliases=['remove', 'drop'], help='deletes the specified track', description=',del ') -@commands.has_role('tm') -async def del_command(ctx: commands.Context, position: int): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if not vc.queue.is_empty: - if position <= 0: - return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=embed_color)) - elif position > song_count: - return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range', color=embed_color)) - else: - SongToBeDeleted = vc.queue._queue[position-1].title - del vc.queue._queue[position-1] - return await ctx.send(embed=nextcord.Embed(description=f'`{SongToBeDeleted}` removed from the QUEUE', color=embed_color)) - else: - return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='skipto',aliases=['goto'], help='skips to the specified track', description=',skipto ') -@commands.has_role('tm') -async def skipto_command(ctx: commands.Context, position: int): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if not vc.queue.is_empty: - if position <= 0: - return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=embed_color)) - elif position > song_count: - return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range', color=embed_color)) - elif position == vc.queue._queue[position-1]: - return await ctx.send(embed=nextcord.Embed(description='Already in that `Position`!', color=embed_color)) - else: - vc.queue.put_at_front(vc.queue._queue[position-1]) - del vc.queue._queue[position] - return await skip_command(ctx) - else: - return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='move', aliases=['set'], help='moves the track to the specified position', description=',move ') -@commands.has_role('tm') -async def move_command(ctx: commands.Context, song_position: int, move_position: int): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if not vc.queue.is_empty: - if song_position <= 0 or move_position <= 0: - return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=embed_color)) - elif song_position > song_count or move_position > song_count: - position = song_position if song_position > song_count else move_position - return await ctx.send(embed=nextcord.Embed(description=f'Position `{position}` is outta range!', color=embed_color)) - elif song_position == move_position: - return await ctx.send(embed=nextcord.Embed(description=f'Already in that `Position`:{move_position}', color=embed_color)) - else: - move_index = move_position-1 if move_position < song_position else move_position - song_index = song_position if move_position < song_position else song_position-1 - vc.queue.put_at_index(move_index, vc.queue._queue[song_position-1]) - moved_song = vc.queue._queue[song_index] - del vc.queue._queue[song_index] - moved_song_name = moved_song.info['title'] - return await ctx.send(embed=nextcord.Embed(description=f'**{moved_song_name}** moved at Position:`{move_position}`', color=embed_color)) - else: - return await ctx.send(embed=nextcord.Embed(description='No songs in the `QUEUE`!', color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='volume',aliases=['vol'], help='sets the volume', description=',vol ') -@commands.has_role('tm') -async def volume_command(ctx: commands.Context, playervolume: int): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if vc.is_connected(): - if playervolume > 100: - return await ctx.send(embed= nextcord.Embed(description='**VOLUME** supported upto `100%`', color=embed_color)) - elif playervolume < 0: - return await ctx.send(embed= nextcord.Embed(description='**VOLUME** can not be `negative`', color=embed_color)) - else: - await ctx.send(embed=nextcord.Embed(description=f'**VOLUME**\nSet to `{playervolume}%`', color=embed_color)) - return await vc.set_volume(playervolume) - elif not vc.is_connected(): - return await ctx.send(embed=nextcord.Embed(description="Player not connected!", color=embed_color)) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='seek', aliases=[], help='seeks or moves the player to specified track position', description=',seek ') -@commands.has_role('tm') -async def seek_command(ctx: commands.Context, seekPosition: int): - if await user_connectivity(ctx) == False: - return - else: - vc: wavelink.Player = ctx.voice_client - if not vc.is_playing(): - return await ctx.send(embed=nextcord.Embed(description='Player not playing!', color=embed_color)) - elif vc.is_playing(): - if 0 <= seekPosition <= vc.track.length: - msg = await ctx.send(embed=nextcord.Embed(description='seeking...', color=embed_color)) - await vc.seek(seekPosition*1000) - return await msg.edit(embed=nextcord.Embed(description=f'Player SEEKED: `{seekPosition}` seconds',color=embed_color)) - else: - return await ctx.send(embed=nextcord.Embed(description=f'SEEK length `{seekPosition}` outta range', color=embed_color)) - -@commands.cooldown(1, 5, commands.BucketType.user) -@commands.command(name='clear',aliases=[], help='clears the queue', description=',clear') -@commands.has_role('tm') -async def clear_command(ctx: commands.Context): - vc: wavelink.Player = ctx.voice_client - if await user_connectivity(ctx) == False: - return - else: - if vc.queue.is_empty: - return await ctx.send(embed= nextcord.Embed(description='No `SONGS` are present', color=embed_color)) - else: - vc.queue._queue.clear() - vc.lq = False - clear_command_embed = nextcord.Embed(description=f'`QUEUE` cleared', color=embed_color) - return await ctx.send(embed=clear_command_embed) - -@commands.cooldown(1, 2, commands.BucketType.user) -@commands.command(name='save', aliases=['dm'], description=",save\n,save ", help='dms the current | specified song to the user') -async def save_command(ctx: commands.Context, savestr: Optional[str]): - vc: wavelink.Player = ctx.voice_client - if await user_connectivity(ctx) == False: + try: + await bot.load_extension('cogs.utility') + print('✓ Loaded utility cog') + except Exception as e: + print(f'✗ Failed to load utility cog: {e}') + +# Run the bot +async def main(): + """Main function to start the bot""" + if not token: + print("ERROR: DISCORD_TOKEN not found in environment variables!") + print("Please create a .env file with your Discord bot token.") return - else: - user = await client.fetch_user(ctx.author._user.id) - if vc._source and savestr is None: - await user.send(embed=nextcord.Embed(description=f'`{vc._source}`', color=embed_color)) - elif not vc.queue.is_empty and savestr == 'q' or savestr == 'queue': - await user.send(embed=qem) - elif not vc.queue.is_empty and savestr: - if int(savestr) <= 0: - return await ctx.send(embed=nextcord.Embed(description=f'Position can not be `ZERO`* or `LESSER`', color=embed_color)) - elif int(savestr) > song_count: - return await ctx.send(embed=nextcord.Embed(description=f'Position `{savestr}` is outta range', color=embed_color)) - else: - song_info = vc.queue._queue[int(savestr) - 1] - em=nextcord.Embed(description=song_info.info['title'], color=embed_color) - await user.send(embed=em) - else: - return await ctx.send(embed=nextcord.Embed(description='There is no `song` | `queue` available', color=embed_color)) - - -# @commands.cooldown(1,2, commands.BucketType.user) -# @commands.command(name='lyrics', aliases=['l'], description=",lyrics | ,l", help='searches the lyrics for current song being played') -# async def lyrics_command(ctx: commands.Context): -# vc: wavelink.Player = ctx.voice_client -# if await user_connectivity(ctx) == False: -# return -# else: -# mylyrics = [] -# genius = lyricsgenius.Genius(access_token=os.environ['lyrics_token']) -# songstr = vc.track.title -# searchmssg = await ctx.send(embed=nextcord.Embed(description=f'**searching the lyrics for {vc.track.title}...**', color = embed_color)) -# if '-' and '(' in songstr: -# song = songstr.split(' - ')[1].split('(')[0] -# author = songstr.split(' - ')[0] -# elif '-' and '[' in songstr: -# song = songstr.split(' - ')[1].split('[')[0] -# author = songstr.split(' - ')[0] -# elif '-' and '|' in songstr: -# song = songstr.split(' - ')[1] -# author = songstr.split(' - ')[0] -# elif '|' in songstr: -# song = songstr.split('|')[0] -# author = songstr.split('|')[1] -# else: -# song = songstr -# author = vc.track.author -# # genius.verbose = False # Turn off status messages -# genius.remove_section_headers = True -# songvalue = genius.search_song(song, author) -# mylyrics.append(songvalue.lyrics) -# if mylyrics is not None: -# for i in mylyrics: -# await ctx.send(embed=nextcord.Embed(description=f'{i}', color=embed_color)) -# await searchmssg.edit(embed=nextcord.Embed(description='**Search found!**', color=embed_color)) -# else: -# await searchmssg.edit(embed=nextcord.Embed(description='**No lyrics found!**', color=embed_color)) -'''main''' - - -@commands.command() -async def add_fav(ctx, account, symbol): - FUT_SYMBOLS = [sym['symbol'] for sym in binanceClient.futures_exchange_info()['symbols']] - SPOT_SYMBOLS = [sym['symbol'] for sym in binanceClient.get_all_tickers()] - if account.upper() == "FUT": - if symbol in FUT_SYMBOLS: - FAV_LIST['FUTURES'][symbol] = {} - else: - await ctx.send("Provided SYMBOL or CRYPTO is not available in Futures") - elif account.upper() == "SPOT": - if symbol in SPOT_SYMBOLS: - FAV_LIST['SPOT'][symbol] = {} - else: - await ctx.send("Provided SYMBOL or CRYPTO is not available in SPOT") - else: - await ctx.send('Provided Account Type is not valid. Please use FUT for Futures and SPOT for spot') - with open('FAV_LIST.json','w') as f: - json.dump(FAV_LIST, f) - - -@commands.command() -async def favs(ctx): - message = "FUTURES FAVOURITE LIST\n" - for i, symbol in enumerate(FAV_LIST['FUTURES'].keys()): - message += str(i+1) + ". " + symbol + "--> Last Price: "+ binanceClient.get_ticker(symbol=symbol)['lastPrice']+"\n" - message += "\n\nSPOT FAVOURITE LIST" - for i, symbol in enumerate(FAV_LIST['SPOT'].keys()): - message += str(i+1) + ". " + symbol + "--> Last Price: "+ binanceClient.get_ticker(symbol=symbol)['lastPrice']+ "\n" - await ctx.send(message) - -@commands.command() -async def fubln(ctx): - balance_list = binanceClient.futures_account_balance() - message = "-"*35 + "\n" - message += "-"*3 + "ACCOUNT BALANCE" + "-"*3 + "\n" - message += "-"*35 +"\n" - for balance in balance_list: - message += balance['asset']+" : "+balance['balance']+"\n" - message += "-"*35 - await ctx.send(message) - - -@tasks.loop(seconds=60) -async def futures_position_alerts(): - futures_info = binanceClient.futures_account() - positions_info = binanceClient.futures_position_information() - positions = futures_info['positions'] - message_channel = await client.fetch_channel(channel_id) - print(f"Got channel {message_channel} for {channel_id}") - if float(futures_info['totalMaintMargin'])/float(futures_info['totalMarginBalance']) > 40.0: - await message_channel.send("Your positions' Margin Ratio is greater than 40%. Please consider taking a look at it.") - for position in positions: - symbol = position['symbol'] - alert = False - message = "------"+symbol+" POSITION ALERT!------\n" - position_info = list(filter(lambda f:(f['symbol']==symbol),positions_info))[0] - if float(position_info['positionAmt']) != 0.0: - if float(position['unrealizedProfit']) < -1.0 : - message += "Unrealized Profit is going down! LOSS : "+ str(position['unrealizedProfit']) +"\n" - alert = True - if (float(position_info['markPrice'])-float(position_info['liquidationPrice']))/(float(position_info['entryPrice'])-float(position_info['liquidationPrice'])) <= 0.4: - message += "Mark price is moving closer to Liquidation Price. Your position may be liquidated soon.\n Mark Price:"+ str(position_info['markPrice']) +"\n Liquidation Price:"+str(position_info['liquidationPrice'])+"\n" - alert = True - if alert: - await message_channel.send(message) + # Load all cogs before starting + async with bot: + await load_cogs() + await bot.start(token) -@futures_position_alerts.before_loop -async def before(): - await client.wait_until_ready() - print("Finished waiting") - -#futures_position_alerts.start() - -#@tasks.loop(seconds=60) -#async def favs_info(): -# message = "INFO of Favourite Crytos\n\n" -# message += "FUTURES\n" -# for i, symbol in enumerate(FAV_LIST['FUTURES'].keys()): -# position = get_future_position(symbol) -# message += str(i)+". "+position['symbol']+" --> unrealizedProfit : "+position['unrealizedProfit'] -# message_channel = await client.fetch_channel(channel_id) -# print(f"Got channel {message_channel} for {channel_id}") -# await message_channel.send(message) - -#@favs_info.before_loop -#async def before(): -# await client.wait_until_ready() -# print("Finished waiting") - -#favs_info.start() - -# MODERATION COMMANDS # -@commands.command() -@has_permissions(kick_members=True, administrator=True) -async def kick(ctx, member:discord.Member,*,reason=None): - guild = ctx.guild - memberKick = discord.Embed(title='Kicked', description = f'You have been kicked from {guild.name} for {reason}') - - await member.kick(reason=reason) - await ctx.send(f'User {member} has been kicked.') - - -@commands.command() -@has_permissions(ban_members=True, administrator=True) -async def ban(ctx, member:discord.Member,*,reason=None,): - guild = ctx.guild - memberBan = discord.Embed(title = 'Banned', description=f'You were banned from {guild.name} for {reason}') - - await member.ban(reason=reason) - await ctx.send(f'User {member} has been banned.') - await member.send(embed=memberBan) - - -@commands.command() -@has_permissions(ban_members=True, administrator=True) -async def unban(self, ctx, *, member:discord.Member): - banned_users = await ctx.guild.bans() - member_name, member_discriminator = member.split('#') - - for ban_entry in banned_users: - user = ban_entry.user - - if (user.name, user.discriminator) == (member_name, member_discriminator): - await ctx.guild.unban(user) - await ctx.send(f'{user.name}#{user.discriminator} has been unbanned.') - return - - -@commands.command(pass_context=True) -@has_permissions(manage_messages=True) -async def mute(ctx,member:discord.Member, reason = None): - guild = ctx.guild - mutedRole = discord.utils.get(guild.roles, name='Muted') - memberMute = discord.Embed(title = 'Muted', description=f'You have been muted from {guild.name} for {reason}') - if mutedRole not in guild.roles: - perms = discord.Permissions(send_messages=False, speak=False) - await guild.create_role(name='Muted', permissions=perms) - await member.add_roles(mutedRole) - await ctx.send('Succesfuly created the [Muted] role and properly assigned it to the user.') - await ctx.add_role(member, mutedRole) - embed=discord.Embed(title='User muted!', description=f'**{0}** was muted by **{1}**!'.format(member, ctx.message.author, color=0xff00f6)) - - -@commands.command(pass_context=True) -@has_permissions(manage_messages=True) -async def unmute(ctx, member:discord.Member, *, reason=None): - guild = ctx.guild - mutedRole = discord.utils.get(guild.roles, name = 'Muted') - - memberUnmute = discord.Embed(title = 'Unmuted', description = f'You were unmuted from {guild.name} for {reason}') - - await member.remove_roles(mutedRole) - await ctx.send(f'Unmuted {member.mention} for {reason}') - await member.send(embed=memberUnmute) - - -client.run(token) +if __name__ == '__main__': + try: + import asyncio + asyncio.run(main()) + except KeyboardInterrupt: + print("\nBot stopped by user") + except Exception as e: + logger.error(f"Fatal error: {e}") + print(f"Failed to start bot: {e}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b0ca2c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# Discord Bot Libraries +discord.py>=2.0.0 +nextcord>=2.0.0 + +# Environment Variables +python-dotenv>=1.0.0 + +# Music/Media +wavelink>=2.0.0 + +# Trading/Binance +python-binance>=1.0.0 + +# Data Processing +numpy>=1.24.0 + +# Lyrics +lyricsgenius>=5.0.0