Upload 94 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- .gitignore +3 -0
- README.md +174 -39
- bot/__pycache__/config.cpython-311.pyc +0 -0
- bot/__pycache__/database.cpython-311.pyc +0 -0
- bot/__pycache__/emojis.cpython-311.pyc +0 -0
- bot/__pycache__/i18n.cpython-311.pyc +0 -0
- bot/__pycache__/server.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/__init__.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/admin.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/ai_admin.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/ai_suite.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/board_games.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/community.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/configuration.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/developer.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/engagement.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/events.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/fun.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/gambling.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/language.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/media.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/media_helpers.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/menu.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/observability.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/server_manager.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/utility.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/verification.cpython-311.pyc +0 -0
- bot/cogs/admin.py +249 -241
- bot/cogs/ai_admin.py +863 -616
- bot/cogs/ai_suite.py +0 -0
- bot/cogs/board_games.py +825 -824
- bot/cogs/community.py +0 -0
- bot/cogs/configuration.py +637 -636
- bot/cogs/developer.py +124 -21
- bot/cogs/engagement.py +0 -0
- bot/cogs/events.py +0 -0
- bot/cogs/fun.py +981 -980
- bot/cogs/gambling.py +9 -8
- bot/cogs/language.py +188 -187
- bot/cogs/media.py +0 -0
- bot/cogs/media_helpers.py +10 -5
- bot/cogs/menu.py +192 -172
- bot/cogs/observability.py +86 -85
- bot/cogs/server_manager.py +7 -5
- bot/cogs/utility.py +556 -358
- bot/cogs/verification.py +178 -177
- bot/config.py +37 -5
- bot/database.py +23 -0
- bot/emojis.py +12 -4
.gitattributes
CHANGED
|
@@ -40,3 +40,5 @@ bot/cogs/__pycache__/media.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
|
| 40 |
database.db filter=lfs diff=lfs merge=lfs -text
|
| 41 |
bot/cogs/__pycache__/admin.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 42 |
bot/cogs/__pycache__/events.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 40 |
database.db filter=lfs diff=lfs merge=lfs -text
|
| 41 |
bot/cogs/__pycache__/admin.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 42 |
bot/cogs/__pycache__/events.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
codex[[:space:]]resume[[:space:]]019d6e48-2f09-7771-b4ab-f2399bb70a8d filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
database.db-wal filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -178,3 +178,6 @@ cookies.txt
|
|
| 178 |
youtube.token.json
|
| 179 |
*.token.json
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
youtube.token.json
|
| 179 |
*.token.json
|
| 180 |
|
| 181 |
+
|
| 182 |
+
.qwen/
|
| 183 |
+
gha-creds-*.json
|
README.md
CHANGED
|
@@ -6,68 +6,203 @@ colorTo: purple
|
|
| 6 |
sdk: docker
|
| 7 |
sdk_version: "20.10.25"
|
| 8 |
python_version: "3.11"
|
| 9 |
-
app_file: main.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
-
# fitness
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
|
|
|
| 23 |
```bash
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
```
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
##
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
```
|
| 41 |
|
| 42 |
-
|
| 43 |
|
| 44 |
-
|
| 45 |
-
- `FITAI_AI_PROVIDER=openai_compatible` لأي مزود يدعم OpenAI Chat API (مثل OpenAI/OpenRouter/Groq...).
|
| 46 |
-
- `FITAI_AI_BASE_URL` لعنوان المزود عند استخدام `openai_compatible`.
|
| 47 |
-
- `FITAI_AI_API_KEY` أو مفاتيح منفصلة `FITAI_COACH_API_KEY` / `FITAI_DEV_API_KEY`.
|
| 48 |
-
- `FITAI_AI_MODEL_FAST/SMART/FALLBACK` أو تخصيص لكل role عبر `FITAI_COACH_MODEL_*` و`FITAI_DEV_MODEL_*`.
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
- توليد البرنامج: `POST /program/generate` ثم `GET /program/status`
|
| 54 |
-
- الإحصائيات: `POST /stats/body` و `GET /stats/body`
|
| 55 |
-
- الجلسات: `POST /session/start` و `POST /session/{id}/complete`
|
| 56 |
-
- ماء اليوم: `GET /water/today` و `POST /water/log`
|
| 57 |
-
- الداشبورد الموحد: `GET /dashboard`
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
| 62 |
-
- يوجد ملف قاعدة بيانات SQLite داخل `fitness_app/fitness.db`.
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
##
|
| 66 |
|
| 67 |
-
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
sdk_version: "20.10.25"
|
| 8 |
python_version: "3.11"
|
| 9 |
+
app_file: bot/main.py
|
| 10 |
pinned: false
|
| 11 |
---
|
|
|
|
| 12 |
|
| 13 |
+
# 🤖 Ultimate AI & Music Discord Bot
|
| 14 |
+
|
| 15 |
+
A feature-rich, multi-purpose Discord bot with cyberpunk aesthetics, AI integration, music playback, economy system, and more.
|
| 16 |
+
|
| 17 |
+
## ✨ Features
|
| 18 |
+
|
| 19 |
+
### 🎵 Music System
|
| 20 |
+
- Full music playback with Wavelink/Lavalink
|
| 21 |
+
- Queue management, filters, 24/7 mode
|
| 22 |
+
- Playlist support and lyrics display
|
| 23 |
+
- Rich music control panels with buttons
|
| 24 |
+
|
| 25 |
+
### 🤖 AI Suite
|
| 26 |
+
- OpenRouter & Gemini AI integration
|
| 27 |
+
- Auto-chat mode in dedicated channels
|
| 28 |
+
- Image analysis and prompt generation
|
| 29 |
+
- Multiple AI personalities (Wise, Sarcastic, Technical, Funny)
|
| 30 |
+
|
| 31 |
+
### 💰 Economy & Engagement
|
| 32 |
+
- Wallet, bank, daily rewards
|
| 33 |
+
- Work, gamble, rob, shop system
|
| 34 |
+
- XP/leveling with rank cards
|
| 35 |
+
- Leaderboards and inventory
|
| 36 |
+
|
| 37 |
+
### 🎮 Games & Fun
|
| 38 |
+
- Trivia (Gaming, Movies, Series)
|
| 39 |
+
- Arcade games (Mario challenge)
|
| 40 |
+
- Board games (Chess, Checkers, Connect 4, Othello)
|
| 41 |
+
- Classic games (XO, 8ball, Slots, Dice, Coinflip)
|
| 42 |
+
|
| 43 |
+
### 🛡️ Moderation & Admin
|
| 44 |
+
- Ban, kick, mute, warn, purge
|
| 45 |
+
- Slowmode, lock/unlock channels
|
| 46 |
+
- Server backup and restore
|
| 47 |
+
- Auto-moderation features
|
| 48 |
+
|
| 49 |
+
### 🌐 Multi-Language Support
|
| 50 |
+
- 14+ languages: Arabic, English, French, Spanish, German, etc.
|
| 51 |
+
- Full i18n with emoji placeholders
|
| 52 |
+
- Per-server language configuration
|
| 53 |
+
|
| 54 |
+
### 🎫 Community Features
|
| 55 |
+
- Ticket system for support
|
| 56 |
+
- Polls and suggestions
|
| 57 |
+
- Giveaways with auto-winner selection
|
| 58 |
+
- Verification system
|
| 59 |
+
|
| 60 |
+
### 📊 Configuration
|
| 61 |
+
- Per-server settings panel
|
| 62 |
+
- Custom channels for logs, welcome, daily messages
|
| 63 |
+
- DJ role, premium tiers, music autoplay
|
| 64 |
+
- Game news and free games alerts
|
| 65 |
+
|
| 66 |
+
## 🚀 Quick Start
|
| 67 |
+
|
| 68 |
+
### Prerequisites
|
| 69 |
+
- Python 3.11+
|
| 70 |
+
- Discord Bot Token ([Discord Developer Portal](https://discord.com/developers/applications))
|
| 71 |
+
- Optional: OpenRouter API key, Gemini API key
|
| 72 |
+
|
| 73 |
+
### Installation
|
| 74 |
+
|
| 75 |
+
1. **Clone the repository**
|
| 76 |
+
```bash
|
| 77 |
+
git clone <your-repo-url>
|
| 78 |
+
cd BOT-
|
| 79 |
+
```
|
| 80 |
|
| 81 |
+
2. **Install dependencies**
|
| 82 |
+
```bash
|
| 83 |
+
pip install -r requirements.txt
|
| 84 |
+
```
|
| 85 |
|
| 86 |
+
3. **Set up environment variables**
|
| 87 |
+
```bash
|
| 88 |
+
cp .env.example .env
|
| 89 |
+
```
|
| 90 |
|
| 91 |
+
Edit `.env` with your configuration:
|
| 92 |
+
```env
|
| 93 |
+
DISCORD_TOKEN=your_bot_token_here
|
| 94 |
+
PREFIX=!
|
| 95 |
+
OWNER_IDS=your_discord_user_id
|
| 96 |
+
OPENROUTER_API_KEY=your_openrouter_key
|
| 97 |
+
GEMINI_API_KEY=your_gemini_key
|
| 98 |
+
```
|
| 99 |
|
| 100 |
+
4. **Run the bot**
|
| 101 |
```bash
|
| 102 |
+
python -m bot.main
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## 🐳 Docker Deployment
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
docker build -t discord-bot .
|
| 109 |
+
docker run -d --env-file .env discord-bot
|
| 110 |
```
|
| 111 |
|
| 112 |
+
## 🌐 Cloud Deployment
|
| 113 |
|
| 114 |
+
### HuggingFace Spaces
|
| 115 |
+
- Push your code to a HF Space
|
| 116 |
+
- Set environment variables in Space settings
|
| 117 |
+
- The bot will auto-start on deployment
|
| 118 |
|
| 119 |
+
### Render
|
| 120 |
+
- Connect your GitHub repository
|
| 121 |
+
- Use the `render.yaml` configuration
|
| 122 |
+
- Add environment variables in Render dashboard
|
| 123 |
|
| 124 |
+
## ⚙️ Configuration
|
| 125 |
|
| 126 |
+
### Required Environment Variables
|
| 127 |
+
| Variable | Description | Example |
|
| 128 |
+
|----------|-------------|---------|
|
| 129 |
+
| `DISCORD_TOKEN` | Your Discord bot token | `MTIz...` |
|
| 130 |
+
| `PREFIX` | Command prefix | `!` or `/` |
|
| 131 |
+
| `OWNER_IDS` | Comma-separated owner user IDs | `123456789,987654321` |
|
| 132 |
+
|
| 133 |
+
### Optional Environment Variables
|
| 134 |
+
| Variable | Description | Default |
|
| 135 |
+
|----------|-------------|---------|
|
| 136 |
+
| `OPENROUTER_API_KEY` | OpenRouter API key for AI | - |
|
| 137 |
+
| `GEMINI_API_KEY` | Google Gemini API key | - |
|
| 138 |
+
| `OPENROUTER_MODEL` | Default AI model | `meta-llama/llama-3.3-70b-instruct:free` |
|
| 139 |
+
| `DB_PATH` | Custom database path | `database.db` |
|
| 140 |
+
| `HF_TOKEN` | HuggingFace token for DB sync | - |
|
| 141 |
+
| `HF_DB_REPO_ID` | HuggingFace repo for cloud sync | - |
|
| 142 |
+
| `PORT` | Keep-alive server port | `10000` |
|
| 143 |
+
|
| 144 |
+
## 📁 Project Structure
|
| 145 |
+
|
| 146 |
+
```
|
| 147 |
+
BOT-/
|
| 148 |
+
├── bot/
|
| 149 |
+
│ ├── cogs/ # Bot modules (cogs)
|
| 150 |
+
│ │ ├── admin.py
|
| 151 |
+
│ │ ├── ai_suite.py
|
| 152 |
+
│ │ ├── media.py
|
| 153 |
+
│ │ ├── community.py
|
| 154 |
+
│ │ └── ...
|
| 155 |
+
│ ├── utils/ # Utility modules
|
| 156 |
+
│ ├── locales/ # Translation files
|
| 157 |
+
│ ├── main.py # Bot entry point
|
| 158 |
+
│ ├── config.py # Configuration loader
|
| 159 |
+
│ ├── database.py # Database layer
|
| 160 |
+
│ ├── emojis.py # Emoji system
|
| 161 |
+
│ ├── i18n.py # Translations
|
| 162 |
+
│ └── theme.py # Styling & colors
|
| 163 |
+
├── requirements.txt
|
| 164 |
+
├── Dockerfile
|
| 165 |
+
├── render.yaml
|
| 166 |
+
├── .env.example
|
| 167 |
+
└── README.md
|
| 168 |
```
|
| 169 |
|
| 170 |
+
## 🎮 Available Commands
|
| 171 |
|
| 172 |
+
Once the bot is running, use `<prefix>menu` or `/menu` to see all available commands organized by category:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
- **Music**: play, pause, skip, queue, volume, filters
|
| 175 |
+
- **Admin**: ban, kick, mute, warn, purge, backup
|
| 176 |
+
- **Fun**: 8ball, trivia, meme, roll, mario
|
| 177 |
+
- **AI**: chat, ask_image, imagine, summarize
|
| 178 |
+
- **Economy**: balance, daily, work, gamble, shop
|
| 179 |
+
- **Utility**: serverinfo, userinfo, ping, remind
|
| 180 |
+
- **Configuration**: set, language, setup
|
| 181 |
|
| 182 |
+
## 🛡️ Security Notes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
- Never share your `DISCORD_TOKEN` or API keys
|
| 185 |
+
- Use `.env` file for sensitive configuration
|
| 186 |
+
- Set `OWNER_IDS` to restrict admin commands
|
| 187 |
+
- The bot uses SQLite with WAL mode for data integrity
|
| 188 |
|
| 189 |
+
## 🤝 Contributing
|
|
|
|
| 190 |
|
| 191 |
+
1. Fork the repository
|
| 192 |
+
2. Create a feature branch
|
| 193 |
+
3. Make your changes
|
| 194 |
+
4. Submit a pull request
|
| 195 |
|
| 196 |
+
## 📝 License
|
| 197 |
|
| 198 |
+
This project is provided as-is for educational and community use.
|
| 199 |
|
| 200 |
+
## 🆘 Support
|
| 201 |
+
|
| 202 |
+
- Open an issue on GitHub for bug reports
|
| 203 |
+
- Check the wiki for common problems
|
| 204 |
+
- Use the bot's `/help` command for usage info
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
|
| 208 |
+
**Developed with ❤️ by Motaz**
|
bot/__pycache__/config.cpython-311.pyc
CHANGED
|
Binary files a/bot/__pycache__/config.cpython-311.pyc and b/bot/__pycache__/config.cpython-311.pyc differ
|
|
|
bot/__pycache__/database.cpython-311.pyc
CHANGED
|
Binary files a/bot/__pycache__/database.cpython-311.pyc and b/bot/__pycache__/database.cpython-311.pyc differ
|
|
|
bot/__pycache__/emojis.cpython-311.pyc
CHANGED
|
Binary files a/bot/__pycache__/emojis.cpython-311.pyc and b/bot/__pycache__/emojis.cpython-311.pyc differ
|
|
|
bot/__pycache__/i18n.cpython-311.pyc
CHANGED
|
Binary files a/bot/__pycache__/i18n.cpython-311.pyc and b/bot/__pycache__/i18n.cpython-311.pyc differ
|
|
|
bot/__pycache__/server.cpython-311.pyc
CHANGED
|
Binary files a/bot/__pycache__/server.cpython-311.pyc and b/bot/__pycache__/server.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/__init__.cpython-311.pyc and b/bot/cogs/__pycache__/__init__.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/admin.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4e65407e3a0ee2ebbaee76d78967fa971bfa4460c8922ad8f4b67abac8670f4e
|
| 3 |
+
size 125088
|
bot/cogs/__pycache__/ai_admin.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/ai_admin.cpython-311.pyc and b/bot/cogs/__pycache__/ai_admin.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/ai_suite.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a59d6c99f1b5da223c1cd0cc22d7e3d6200ea5f6c00df433181311389f2b584c
|
| 3 |
+
size 142874
|
bot/cogs/__pycache__/board_games.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/board_games.cpython-311.pyc and b/bot/cogs/__pycache__/board_games.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/community.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:201e9aaaccda31150c69164ad13e5e8c8ef4378ea00255cd1e3a249aeb46841c
|
| 3 |
+
size 131426
|
bot/cogs/__pycache__/configuration.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/configuration.cpython-311.pyc and b/bot/cogs/__pycache__/configuration.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/developer.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/developer.cpython-311.pyc and b/bot/cogs/__pycache__/developer.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/engagement.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a7edcd122436aaa7322f06746c36814bf9a66e19bf6dd777a080bfb682dbce5d
|
| 3 |
+
size 121567
|
bot/cogs/__pycache__/events.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:58e4643fd664dcdb77bde357abcf03ef9507fb718cb6a8aa0a575b041c7a7102
|
| 3 |
+
size 141527
|
bot/cogs/__pycache__/fun.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/fun.cpython-311.pyc and b/bot/cogs/__pycache__/fun.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/gambling.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/gambling.cpython-311.pyc and b/bot/cogs/__pycache__/gambling.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/language.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/language.cpython-311.pyc and b/bot/cogs/__pycache__/language.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/media.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a4041ede6a563d0c1605ccde48c9572c66d76bbbaa74f4a7e64abc807651d3a
|
| 3 |
+
size 223657
|
bot/cogs/__pycache__/media_helpers.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/media_helpers.cpython-311.pyc and b/bot/cogs/__pycache__/media_helpers.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/menu.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/menu.cpython-311.pyc and b/bot/cogs/__pycache__/menu.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/observability.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/observability.cpython-311.pyc and b/bot/cogs/__pycache__/observability.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/server_manager.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/server_manager.cpython-311.pyc and b/bot/cogs/__pycache__/server_manager.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/utility.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/utility.cpython-311.pyc and b/bot/cogs/__pycache__/utility.cpython-311.pyc differ
|
|
|
bot/cogs/__pycache__/verification.cpython-311.pyc
CHANGED
|
Binary files a/bot/cogs/__pycache__/verification.cpython-311.pyc and b/bot/cogs/__pycache__/verification.cpython-311.pyc differ
|
|
|
bot/cogs/admin.py
CHANGED
|
@@ -19,6 +19,7 @@ from bot.theme import (
|
|
| 19 |
NEON_BLUE, NEON_YELLOW, panel_divider, success_embed, error_embed, warning_embed, info_embed,
|
| 20 |
double_line, triple_line, shimmer, pick_neon_color, add_banner_to_embed
|
| 21 |
)
|
|
|
|
| 22 |
from bot.emojis import (
|
| 23 |
ui, E_SHIELD, E_CROWN, E_TROPHY, E_FIRE, E_SPARKLE, E_LOCK, E_KEY,
|
| 24 |
E_ARROW_BLUE, E_ARROW_GREEN, E_ARROW_PINK, E_ARROW_PURPLE, E_GEM, E_STAR
|
|
@@ -29,7 +30,7 @@ from bot.emojis import (
|
|
| 29 |
# AWESOME ROLES DEFINITIONS
|
| 30 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 31 |
|
| 32 |
-
AWESOME_ROLES = [
|
| 33 |
{
|
| 34 |
"name": "✨ Cyan Legend",
|
| 35 |
"color": NEON_CYAN,
|
|
@@ -79,25 +80,25 @@ AWESOME_ROLES = [
|
|
| 79 |
"hoist": True,
|
| 80 |
"description": "Trusted member with azure power"
|
| 81 |
},
|
| 82 |
-
]
|
| 83 |
-
|
| 84 |
-
PRESENCE_STATUS_MAP: dict[str, discord.Status] = {
|
| 85 |
-
"online": discord.Status.online,
|
| 86 |
-
"idle": discord.Status.idle,
|
| 87 |
-
"dnd": discord.Status.dnd,
|
| 88 |
-
"invisible": discord.Status.invisible,
|
| 89 |
-
"offline": discord.Status.invisible,
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
PRESENCE_ACTIVITY_MAP: dict[str, discord.ActivityType] = {
|
| 93 |
-
"playing": discord.ActivityType.playing,
|
| 94 |
-
"watching": discord.ActivityType.watching,
|
| 95 |
-
"listening": discord.ActivityType.listening,
|
| 96 |
-
"competing": discord.ActivityType.competing,
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
class AdminMasterPanel(discord.ui.View):
|
| 101 |
def __init__(self, cog: "Admin") -> None:
|
| 102 |
super().__init__(timeout=None)
|
| 103 |
self.cog = cog
|
|
@@ -128,27 +129,34 @@ class AdminMasterPanel(discord.ui.View):
|
|
| 128 |
)
|
| 129 |
return
|
| 130 |
if choice == "shield":
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
return
|
| 133 |
-
if choice == "status":
|
| 134 |
-
synced = "Enabled" if self.cog.bot.db._hf_sync_enabled else "Disabled"
|
| 135 |
-
activity_obj = getattr(self.cog.bot, "activity", None)
|
| 136 |
-
activity_name = getattr(activity_obj, "name", None) or "CYBER // GRID"
|
| 137 |
-
activity_type = str(getattr(activity_obj, "type", "playing")).replace("ActivityType.", "")
|
| 138 |
-
status_name = str(getattr(self.cog.bot, "status", "online")).replace("Status.", "")
|
| 139 |
-
await interaction.response.send_message(
|
| 140 |
-
(
|
| 141 |
-
f"HF DB Sync: **{synced}**\n"
|
| 142 |
-
f"Bot Status: **{status_name}**\n"
|
| 143 |
-
f"Bot Activity: **{activity_type}** • **{activity_name}**\n\n"
|
| 144 |
-
"Owner commands:\n"
|
| 145 |
-
"`/set_bot_status <status> <activity_type> <text>`\n"
|
| 146 |
-
"`/reset_bot_status`\n"
|
| 147 |
-
"`/bot_status`"
|
| 148 |
-
),
|
| 149 |
-
ephemeral=True,
|
| 150 |
-
)
|
| 151 |
-
return
|
| 152 |
if choice == "giveaways":
|
| 153 |
await interaction.response.send_message(
|
| 154 |
"**Giveaway Commands:**\n"
|
|
@@ -411,8 +419,8 @@ class EmojiCloneModal(discord.ui.Modal):
|
|
| 411 |
if not interaction.user.guild_permissions.manage_emojis:
|
| 412 |
await interaction.response.send_message("Manage Emojis permission required.", ephemeral=True)
|
| 413 |
return
|
| 414 |
-
source = str(self.source.value).strip()
|
| 415 |
-
name = str(self.target_name.value).strip() or None
|
| 416 |
created, error_text = await self.cog._clone_emoji_to_guild(
|
| 417 |
interaction.guild,
|
| 418 |
interaction.user,
|
|
@@ -583,9 +591,9 @@ class Admin(commands.Cog):
|
|
| 583 |
)
|
| 584 |
return before, after
|
| 585 |
|
| 586 |
-
async def _safe_ctx_send(
|
| 587 |
-
self,
|
| 588 |
-
ctx: commands.Context,
|
| 589 |
*,
|
| 590 |
content: str | None = None,
|
| 591 |
embed: discord.Embed | None = None,
|
|
@@ -617,82 +625,82 @@ class Admin(commands.Cog):
|
|
| 617 |
|
| 618 |
if ctx.channel:
|
| 619 |
channel_kwargs = {k: v for k, v in kwargs.items() if k != "ephemeral"}
|
| 620 |
-
msg = await ctx.channel.send(**channel_kwargs)
|
| 621 |
-
if delete_after:
|
| 622 |
-
await msg.delete(delay=delete_after)
|
| 623 |
-
|
| 624 |
-
def _presence_status_label(self, status: discord.Status | str | None) -> str:
|
| 625 |
-
value = str(status or "online").lower()
|
| 626 |
-
return {
|
| 627 |
-
"online": "online",
|
| 628 |
-
"idle": "idle",
|
| 629 |
-
"dnd": "dnd",
|
| 630 |
-
"invisible": "invisible",
|
| 631 |
-
"offline": "invisible",
|
| 632 |
-
}.get(value, "online")
|
| 633 |
-
|
| 634 |
-
def _presence_activity_label(self, activity_type: discord.ActivityType | str | None) -> str:
|
| 635 |
-
value = str(activity_type or "playing").lower()
|
| 636 |
-
return {
|
| 637 |
-
"playing": "playing",
|
| 638 |
-
"watching": "watching",
|
| 639 |
-
"listening": "listening",
|
| 640 |
-
"competing": "competing",
|
| 641 |
-
}.get(value, "playing")
|
| 642 |
-
|
| 643 |
-
async def _set_bot_presence(
|
| 644 |
-
self,
|
| 645 |
-
actor_id: int,
|
| 646 |
-
*,
|
| 647 |
-
status_name: str,
|
| 648 |
-
activity_type_name: str,
|
| 649 |
-
activity_text: str,
|
| 650 |
-
) -> None:
|
| 651 |
-
resolved_status = PRESENCE_STATUS_MAP.get(status_name, discord.Status.online)
|
| 652 |
-
resolved_activity_type = PRESENCE_ACTIVITY_MAP.get(activity_type_name, discord.ActivityType.playing)
|
| 653 |
-
await self.bot.change_presence(
|
| 654 |
-
status=resolved_status,
|
| 655 |
-
activity=discord.Activity(type=resolved_activity_type, name=activity_text),
|
| 656 |
-
)
|
| 657 |
-
await self.bot.db.execute(
|
| 658 |
-
"INSERT INTO bot_presence_config(id, status, activity_type, activity_text, updated_by, updated_at) "
|
| 659 |
-
"VALUES (1, ?, ?, ?, ?, ?) "
|
| 660 |
-
"ON CONFLICT(id) DO UPDATE SET "
|
| 661 |
-
"status = excluded.status, "
|
| 662 |
-
"activity_type = excluded.activity_type, "
|
| 663 |
-
"activity_text = excluded.activity_text, "
|
| 664 |
-
"updated_by = excluded.updated_by, "
|
| 665 |
-
"updated_at = excluded.updated_at",
|
| 666 |
-
status_name,
|
| 667 |
-
activity_type_name,
|
| 668 |
-
activity_text,
|
| 669 |
-
actor_id,
|
| 670 |
-
dt.datetime.utcnow().isoformat(),
|
| 671 |
-
)
|
| 672 |
-
|
| 673 |
-
async def _presence_embed(self) -> discord.Embed:
|
| 674 |
-
current_activity = getattr(self.bot, "activity", None)
|
| 675 |
-
activity_text = getattr(current_activity, "name", None) or "CYBER // GRID"
|
| 676 |
-
activity_type_name = self._presence_activity_label(getattr(current_activity, "type", None))
|
| 677 |
-
status_name = self._presence_status_label(getattr(self.bot, "status", None))
|
| 678 |
-
row = await self.bot.db.fetchone(
|
| 679 |
-
"SELECT updated_by, updated_at FROM bot_presence_config WHERE id = 1"
|
| 680 |
-
)
|
| 681 |
-
updated_by = f"<@{row[0]}>" if row and row[0] else "Unknown"
|
| 682 |
-
updated_at = row[1] if row and row[1] else "Not saved yet"
|
| 683 |
-
embed = info_embed(
|
| 684 |
-
"Bot Presence",
|
| 685 |
-
(
|
| 686 |
-
f"Status: **{status_name}**\n"
|
| 687 |
-
f"Activity: **{activity_type_name}**\n"
|
| 688 |
-
f"Text: **{activity_text}**\n\n"
|
| 689 |
-
f"Updated by: {updated_by}\n"
|
| 690 |
-
f"Updated at: `{updated_at}`"
|
| 691 |
-
),
|
| 692 |
-
)
|
| 693 |
-
return embed
|
| 694 |
-
|
| 695 |
-
@commands.hybrid_command(name="purge")
|
| 696 |
@commands.has_permissions(manage_messages=True)
|
| 697 |
async def purge(self, ctx: commands.Context, amount: int) -> None:
|
| 698 |
"""Delete a number of messages from the channel."""
|
|
@@ -712,7 +720,7 @@ class Admin(commands.Cog):
|
|
| 712 |
embed = success_embed("🗑️ Messages Purged", f"{panel_divider('green')}\n{desc}\n{panel_divider('green')}")
|
| 713 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction), delete_after=5 if not ctx.interaction else None)
|
| 714 |
|
| 715 |
-
@commands.hybrid_command(name="admin_panel", description="
|
| 716 |
@commands.has_permissions(administrator=True)
|
| 717 |
async def admin_panel(self, ctx: commands.Context) -> None:
|
| 718 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
@@ -724,103 +732,103 @@ class Admin(commands.Cog):
|
|
| 724 |
ephemeral=bool(ctx.interaction),
|
| 725 |
)
|
| 726 |
|
| 727 |
-
@commands.hybrid_group(name="admin", fallback="panel", description="Administrative grouped controls")
|
| 728 |
-
@commands.has_permissions(administrator=True)
|
| 729 |
-
async def admin_group(self, ctx: commands.Context) -> None:
|
| 730 |
-
await self.admin_panel(ctx)
|
| 731 |
-
|
| 732 |
-
@admin_group.group(name="botstatus", invoke_without_command=True)
|
| 733 |
-
@commands.is_owner()
|
| 734 |
-
async def admin_botstatus_group(self, ctx: commands.Context) -> None:
|
| 735 |
-
embed = await self._presence_embed()
|
| 736 |
-
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 737 |
-
|
| 738 |
-
@admin_botstatus_group.command(name="set")
|
| 739 |
-
@commands.is_owner()
|
| 740 |
-
async def admin_botstatus_set(
|
| 741 |
-
self,
|
| 742 |
-
ctx: commands.Context,
|
| 743 |
-
status: str,
|
| 744 |
-
activity_type: str,
|
| 745 |
-
*,
|
| 746 |
-
text: str,
|
| 747 |
-
) -> None:
|
| 748 |
-
status_name = (status or "").strip().lower()
|
| 749 |
-
activity_type_name = (activity_type or "").strip().lower()
|
| 750 |
-
activity_text = (text or "").strip()[:128]
|
| 751 |
-
|
| 752 |
-
if status_name not in PRESENCE_STATUS_MAP:
|
| 753 |
-
await self._safe_ctx_send(
|
| 754 |
-
ctx,
|
| 755 |
-
content="Invalid status. Use: `online`, `idle`, `dnd`, `invisible`",
|
| 756 |
-
ephemeral=bool(ctx.interaction),
|
| 757 |
-
)
|
| 758 |
-
return
|
| 759 |
-
if activity_type_name not in PRESENCE_ACTIVITY_MAP:
|
| 760 |
-
await self._safe_ctx_send(
|
| 761 |
-
ctx,
|
| 762 |
-
content="Invalid activity type. Use: `playing`, `watching`, `listening`, `competing`",
|
| 763 |
-
ephemeral=bool(ctx.interaction),
|
| 764 |
-
)
|
| 765 |
-
return
|
| 766 |
-
if not activity_text:
|
| 767 |
-
await self._safe_ctx_send(ctx, content="Activity text cannot be empty.", ephemeral=bool(ctx.interaction))
|
| 768 |
-
return
|
| 769 |
-
|
| 770 |
-
await self._set_bot_presence(
|
| 771 |
-
ctx.author.id,
|
| 772 |
-
status_name=status_name,
|
| 773 |
-
activity_type_name=activity_type_name,
|
| 774 |
-
activity_text=activity_text,
|
| 775 |
-
)
|
| 776 |
-
embed = success_embed(
|
| 777 |
-
"Bot Status Updated",
|
| 778 |
-
(
|
| 779 |
-
f"Status: **{status_name}**\n"
|
| 780 |
-
f"Activity: **{activity_type_name}**\n"
|
| 781 |
-
f"Text: **{activity_text}**"
|
| 782 |
-
),
|
| 783 |
-
)
|
| 784 |
-
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 785 |
-
|
| 786 |
-
@admin_botstatus_group.command(name="reset")
|
| 787 |
-
@commands.is_owner()
|
| 788 |
-
async def admin_botstatus_reset(self, ctx: commands.Context) -> None:
|
| 789 |
-
await self._set_bot_presence(
|
| 790 |
-
ctx.author.id,
|
| 791 |
-
status_name="online",
|
| 792 |
-
activity_type_name="playing",
|
| 793 |
-
activity_text="CYBER // GRID",
|
| 794 |
-
)
|
| 795 |
-
embed = success_embed("Bot Status Reset", "Restored default presence.")
|
| 796 |
-
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 797 |
-
|
| 798 |
-
@commands.hybrid_command(name="bot_status", description="
|
| 799 |
-
@commands.is_owner()
|
| 800 |
-
async def show_bot_status(self, ctx: commands.Context) -> None:
|
| 801 |
-
embed = await self._presence_embed()
|
| 802 |
-
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 803 |
-
|
| 804 |
-
@commands.hybrid_command(name="set_bot_status", description="
|
| 805 |
-
@commands.is_owner()
|
| 806 |
-
async def update_bot_status(
|
| 807 |
-
self,
|
| 808 |
-
ctx: commands.Context,
|
| 809 |
-
status: str,
|
| 810 |
-
activity_type: str,
|
| 811 |
-
*,
|
| 812 |
-
text: str,
|
| 813 |
-
) -> None:
|
| 814 |
-
await self.admin_botstatus_set(ctx, status, activity_type, text=text)
|
| 815 |
-
|
| 816 |
-
@commands.hybrid_command(name="reset_bot_status", description="
|
| 817 |
-
@commands.is_owner()
|
| 818 |
-
async def restore_bot_status(self, ctx: commands.Context) -> None:
|
| 819 |
-
await self.admin_botstatus_reset(ctx)
|
| 820 |
-
|
| 821 |
-
@admin_group.group(name="shield", invoke_without_command=True)
|
| 822 |
-
@commands.has_permissions(administrator=True)
|
| 823 |
-
async def admin_shield_group(self, ctx: commands.Context) -> None:
|
| 824 |
await self._safe_ctx_send(
|
| 825 |
ctx,
|
| 826 |
content="Use `/admin shield state`, `/admin shield set_level <low|medium|high>`, or `/admin shield add_image <attachment>`",
|
|
@@ -855,7 +863,7 @@ class Admin(commands.Cog):
|
|
| 855 |
)
|
| 856 |
await self._safe_ctx_send(ctx, content="✅ Scam image signature saved.", ephemeral=bool(ctx.interaction))
|
| 857 |
|
| 858 |
-
@commands.hybrid_command(name="shield_level", description="
|
| 859 |
@commands.has_permissions(administrator=True)
|
| 860 |
async def shield_level(self, ctx: commands.Context, level: str) -> None:
|
| 861 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
@@ -871,7 +879,7 @@ class Admin(commands.Cog):
|
|
| 871 |
)
|
| 872 |
await self._safe_ctx_send(ctx, content=f"🛡️ Shield level set to `{normalized}`", ephemeral=bool(ctx.interaction))
|
| 873 |
|
| 874 |
-
@commands.hybrid_command(name="shield_state", description="
|
| 875 |
@commands.has_permissions(administrator=True)
|
| 876 |
async def shield_state(self, ctx: commands.Context) -> None:
|
| 877 |
if not ctx.guild:
|
|
@@ -900,7 +908,7 @@ class Admin(commands.Cog):
|
|
| 900 |
)
|
| 901 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 902 |
|
| 903 |
-
@commands.hybrid_command(name="econ_admin", description="
|
| 904 |
@commands.has_permissions(administrator=True)
|
| 905 |
async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None:
|
| 906 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
@@ -922,7 +930,7 @@ class Admin(commands.Cog):
|
|
| 922 |
)
|
| 923 |
await self._safe_ctx_send(ctx, content=f"💰 {member.mention} coins: `{coins}` → `{new_coins}`", ephemeral=bool(ctx.interaction))
|
| 924 |
|
| 925 |
-
@commands.hybrid_command(name="warn")
|
| 926 |
@commands.has_permissions(manage_roles=True)
|
| 927 |
async def warn(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 928 |
"""Warn a member."""
|
|
@@ -970,7 +978,7 @@ class Admin(commands.Cog):
|
|
| 970 |
except discord.Forbidden:
|
| 971 |
pass
|
| 972 |
|
| 973 |
-
@commands.hybrid_command(name="warnings")
|
| 974 |
@commands.has_permissions(manage_roles=True)
|
| 975 |
async def warnings(self, ctx: commands.Context, member: discord.Member) -> None:
|
| 976 |
"""View warnings for a member."""
|
|
@@ -1005,7 +1013,7 @@ class Admin(commands.Cog):
|
|
| 1005 |
embed.set_thumbnail(url=member.display_avatar.url)
|
| 1006 |
await ctx.reply(embed=embed)
|
| 1007 |
|
| 1008 |
-
@commands.hybrid_command(name="clearwarn")
|
| 1009 |
@commands.has_permissions(manage_roles=True)
|
| 1010 |
async def clearwarn(self, ctx: commands.Context, member: discord.Member) -> None:
|
| 1011 |
"""Clear all warnings for a member."""
|
|
@@ -1024,7 +1032,7 @@ class Admin(commands.Cog):
|
|
| 1024 |
embed = success_embed("✅ Warnings Cleared", f"🗑️ All warnings for {member.mention} have been removed.")
|
| 1025 |
await ctx.reply(embed=embed)
|
| 1026 |
|
| 1027 |
-
@commands.hybrid_command(name="kick")
|
| 1028 |
@commands.has_permissions(kick_members=True)
|
| 1029 |
async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1030 |
"""Kick a member from the server."""
|
|
@@ -1069,7 +1077,7 @@ class Admin(commands.Cog):
|
|
| 1069 |
)
|
| 1070 |
await ctx.reply(embed=embed)
|
| 1071 |
|
| 1072 |
-
@commands.hybrid_command(name="ban")
|
| 1073 |
@commands.has_permissions(ban_members=True)
|
| 1074 |
async def ban(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1075 |
"""Ban a member from the server."""
|
|
@@ -1120,7 +1128,7 @@ class Admin(commands.Cog):
|
|
| 1120 |
color=discord.Color.red(),
|
| 1121 |
)
|
| 1122 |
|
| 1123 |
-
@commands.hybrid_command(name="unban")
|
| 1124 |
@commands.has_permissions(ban_members=True)
|
| 1125 |
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = "No reason provided") -> None:
|
| 1126 |
"""Unban a user by their ID."""
|
|
@@ -1178,7 +1186,7 @@ class Admin(commands.Cog):
|
|
| 1178 |
color=discord.Color.green(),
|
| 1179 |
)
|
| 1180 |
|
| 1181 |
-
@commands.hybrid_command(name="mute")
|
| 1182 |
@commands.has_permissions(moderate_members=True)
|
| 1183 |
async def mute(self, ctx: commands.Context, member: discord.Member, duration: int = 10, *, reason: str = "No reason provided") -> None:
|
| 1184 |
"""Timeout a member for a specified duration in minutes."""
|
|
@@ -1225,7 +1233,7 @@ class Admin(commands.Cog):
|
|
| 1225 |
color=discord.Color.red(),
|
| 1226 |
)
|
| 1227 |
|
| 1228 |
-
@commands.hybrid_command(name="unmute")
|
| 1229 |
@commands.has_permissions(moderate_members=True)
|
| 1230 |
async def unmute(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1231 |
"""Remove timeout from a member."""
|
|
@@ -1320,13 +1328,13 @@ class Admin(commands.Cog):
|
|
| 1320 |
if not ctx.guild:
|
| 1321 |
await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction))
|
| 1322 |
return
|
| 1323 |
-
embed = info_embed(
|
| 1324 |
-
"Emoji Clone Panel",
|
| 1325 |
-
"Quick actions:\n- Clone from tag/ID/URL/name\n- View server emojis\n- Open bot emoji picker",
|
| 1326 |
-
)
|
| 1327 |
await self._safe_ctx_send(ctx, embed=embed, view=EmojiClonePanelView(self), ephemeral=bool(ctx.interaction))
|
| 1328 |
|
| 1329 |
-
@commands.hybrid_command(name="emoji_clone_panel", description="
|
| 1330 |
@commands.has_permissions(manage_emojis=True)
|
| 1331 |
async def emoji_clone_panel(self, ctx: commands.Context) -> None:
|
| 1332 |
await self.admin_emoji_panel(ctx)
|
|
@@ -1377,7 +1385,7 @@ class Admin(commands.Cog):
|
|
| 1377 |
)
|
| 1378 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 1379 |
|
| 1380 |
-
@commands.hybrid_command(name="awesomeroles")
|
| 1381 |
@commands.has_permissions(manage_roles=True)
|
| 1382 |
async def awesomeroles(self, ctx: commands.Context) -> None:
|
| 1383 |
"""Create awesome roles for the server with proper permissions."""
|
|
@@ -1442,7 +1450,7 @@ class Admin(commands.Cog):
|
|
| 1442 |
embed.set_footer(text=f"{E_STAR} Created by {ctx.author.display_name}")
|
| 1443 |
await ctx.reply(embed=embed)
|
| 1444 |
|
| 1445 |
-
@commands.hybrid_command(name="backupserver")
|
| 1446 |
@commands.has_permissions(administrator=True)
|
| 1447 |
async def backupserver(self, ctx: commands.Context) -> None:
|
| 1448 |
"""Create a backup of server structure."""
|
|
@@ -1516,7 +1524,7 @@ class Admin(commands.Cog):
|
|
| 1516 |
)
|
| 1517 |
await ctx.reply(embed=embed)
|
| 1518 |
|
| 1519 |
-
@commands.hybrid_command(name="backup_panel", description="
|
| 1520 |
@commands.has_permissions(administrator=True)
|
| 1521 |
async def backup_panel(self, ctx: commands.Context) -> None:
|
| 1522 |
"""Open the interactive backup management panel."""
|
|
@@ -1757,7 +1765,7 @@ class SlowmodeCommandMixin:
|
|
| 1757 |
pass
|
| 1758 |
|
| 1759 |
|
| 1760 |
-
@commands.hybrid_command(name="slowmode")
|
| 1761 |
@commands.has_permissions(manage_channels=True)
|
| 1762 |
async def slowmode(self, ctx: commands.Context, seconds: int = 0) -> None:
|
| 1763 |
"""Set slowmode for the current channel."""
|
|
@@ -1779,7 +1787,7 @@ class SlowmodeCommandMixin:
|
|
| 1779 |
embed = success_embed("⏱️ Slowmode Disabled", f"📺 **Channel:** {ctx.channel.mention}")
|
| 1780 |
await ctx.reply(embed=embed)
|
| 1781 |
|
| 1782 |
-
@commands.hybrid_command(name="lock")
|
| 1783 |
@commands.has_permissions(manage_channels=True)
|
| 1784 |
async def lock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None:
|
| 1785 |
"""Lock a channel to prevent messages."""
|
|
@@ -1797,7 +1805,7 @@ class SlowmodeCommandMixin:
|
|
| 1797 |
embed = success_embed("🔒 Channel Locked", f"📺 **Channel:** {channel.mention}")
|
| 1798 |
await ctx.reply(embed=embed)
|
| 1799 |
|
| 1800 |
-
@commands.hybrid_command(name="unlock")
|
| 1801 |
@commands.has_permissions(manage_channels=True)
|
| 1802 |
async def unlock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None:
|
| 1803 |
"""Unlock a channel to allow messages."""
|
|
|
|
| 19 |
NEON_BLUE, NEON_YELLOW, panel_divider, success_embed, error_embed, warning_embed, info_embed,
|
| 20 |
double_line, triple_line, shimmer, pick_neon_color, add_banner_to_embed
|
| 21 |
)
|
| 22 |
+
from bot.i18n import get_cmd_desc
|
| 23 |
from bot.emojis import (
|
| 24 |
ui, E_SHIELD, E_CROWN, E_TROPHY, E_FIRE, E_SPARKLE, E_LOCK, E_KEY,
|
| 25 |
E_ARROW_BLUE, E_ARROW_GREEN, E_ARROW_PINK, E_ARROW_PURPLE, E_GEM, E_STAR
|
|
|
|
| 30 |
# AWESOME ROLES DEFINITIONS
|
| 31 |
# ═══════════════════════════════════════════════════════════════════════════════
|
| 32 |
|
| 33 |
+
AWESOME_ROLES = [
|
| 34 |
{
|
| 35 |
"name": "✨ Cyan Legend",
|
| 36 |
"color": NEON_CYAN,
|
|
|
|
| 80 |
"hoist": True,
|
| 81 |
"description": "Trusted member with azure power"
|
| 82 |
},
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
PRESENCE_STATUS_MAP: dict[str, discord.Status] = {
|
| 86 |
+
"online": discord.Status.online,
|
| 87 |
+
"idle": discord.Status.idle,
|
| 88 |
+
"dnd": discord.Status.dnd,
|
| 89 |
+
"invisible": discord.Status.invisible,
|
| 90 |
+
"offline": discord.Status.invisible,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
PRESENCE_ACTIVITY_MAP: dict[str, discord.ActivityType] = {
|
| 94 |
+
"playing": discord.ActivityType.playing,
|
| 95 |
+
"watching": discord.ActivityType.watching,
|
| 96 |
+
"listening": discord.ActivityType.listening,
|
| 97 |
+
"competing": discord.ActivityType.competing,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class AdminMasterPanel(discord.ui.View):
|
| 102 |
def __init__(self, cog: "Admin") -> None:
|
| 103 |
super().__init__(timeout=None)
|
| 104 |
self.cog = cog
|
|
|
|
| 129 |
)
|
| 130 |
return
|
| 131 |
if choice == "shield":
|
| 132 |
+
events_cog = self.cog.bot.get_cog("Events")
|
| 133 |
+
if events_cog is None:
|
| 134 |
+
await interaction.response.send_message("❌ Events cog not loaded.", ephemeral=True)
|
| 135 |
+
return
|
| 136 |
+
from bot.cogs.events import ShieldControlPanel
|
| 137 |
+
panel = ShieldControlPanel(events_cog, interaction.guild.id)
|
| 138 |
+
embed = await panel._build_embed()
|
| 139 |
+
await interaction.response.edit_message(embed=embed, view=panel)
|
| 140 |
+
return
|
| 141 |
+
if choice == "status":
|
| 142 |
+
synced = "Enabled" if self.cog.bot.db._hf_sync_enabled else "Disabled"
|
| 143 |
+
activity_obj = getattr(self.cog.bot, "activity", None)
|
| 144 |
+
activity_name = getattr(activity_obj, "name", None) or "CYBER // GRID"
|
| 145 |
+
activity_type = str(getattr(activity_obj, "type", "playing")).replace("ActivityType.", "")
|
| 146 |
+
status_name = str(getattr(self.cog.bot, "status", "online")).replace("Status.", "")
|
| 147 |
+
await interaction.response.send_message(
|
| 148 |
+
(
|
| 149 |
+
f"HF DB Sync: **{synced}**\n"
|
| 150 |
+
f"Bot Status: **{status_name}**\n"
|
| 151 |
+
f"Bot Activity: **{activity_type}** • **{activity_name}**\n\n"
|
| 152 |
+
"Owner commands:\n"
|
| 153 |
+
"`/set_bot_status <status> <activity_type> <text>`\n"
|
| 154 |
+
"`/reset_bot_status`\n"
|
| 155 |
+
"`/bot_status`"
|
| 156 |
+
),
|
| 157 |
+
ephemeral=True,
|
| 158 |
+
)
|
| 159 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
if choice == "giveaways":
|
| 161 |
await interaction.response.send_message(
|
| 162 |
"**Giveaway Commands:**\n"
|
|
|
|
| 419 |
if not interaction.user.guild_permissions.manage_emojis:
|
| 420 |
await interaction.response.send_message("Manage Emojis permission required.", ephemeral=True)
|
| 421 |
return
|
| 422 |
+
source = str(self.source.value).strip()
|
| 423 |
+
name = str(self.target_name.value).strip() or None
|
| 424 |
created, error_text = await self.cog._clone_emoji_to_guild(
|
| 425 |
interaction.guild,
|
| 426 |
interaction.user,
|
|
|
|
| 591 |
)
|
| 592 |
return before, after
|
| 593 |
|
| 594 |
+
async def _safe_ctx_send(
|
| 595 |
+
self,
|
| 596 |
+
ctx: commands.Context,
|
| 597 |
*,
|
| 598 |
content: str | None = None,
|
| 599 |
embed: discord.Embed | None = None,
|
|
|
|
| 625 |
|
| 626 |
if ctx.channel:
|
| 627 |
channel_kwargs = {k: v for k, v in kwargs.items() if k != "ephemeral"}
|
| 628 |
+
msg = await ctx.channel.send(**channel_kwargs)
|
| 629 |
+
if delete_after:
|
| 630 |
+
await msg.delete(delay=delete_after)
|
| 631 |
+
|
| 632 |
+
def _presence_status_label(self, status: discord.Status | str | None) -> str:
|
| 633 |
+
value = str(status or "online").lower()
|
| 634 |
+
return {
|
| 635 |
+
"online": "online",
|
| 636 |
+
"idle": "idle",
|
| 637 |
+
"dnd": "dnd",
|
| 638 |
+
"invisible": "invisible",
|
| 639 |
+
"offline": "invisible",
|
| 640 |
+
}.get(value, "online")
|
| 641 |
+
|
| 642 |
+
def _presence_activity_label(self, activity_type: discord.ActivityType | str | None) -> str:
|
| 643 |
+
value = str(activity_type or "playing").lower()
|
| 644 |
+
return {
|
| 645 |
+
"playing": "playing",
|
| 646 |
+
"watching": "watching",
|
| 647 |
+
"listening": "listening",
|
| 648 |
+
"competing": "competing",
|
| 649 |
+
}.get(value, "playing")
|
| 650 |
+
|
| 651 |
+
async def _set_bot_presence(
|
| 652 |
+
self,
|
| 653 |
+
actor_id: int,
|
| 654 |
+
*,
|
| 655 |
+
status_name: str,
|
| 656 |
+
activity_type_name: str,
|
| 657 |
+
activity_text: str,
|
| 658 |
+
) -> None:
|
| 659 |
+
resolved_status = PRESENCE_STATUS_MAP.get(status_name, discord.Status.online)
|
| 660 |
+
resolved_activity_type = PRESENCE_ACTIVITY_MAP.get(activity_type_name, discord.ActivityType.playing)
|
| 661 |
+
await self.bot.change_presence(
|
| 662 |
+
status=resolved_status,
|
| 663 |
+
activity=discord.Activity(type=resolved_activity_type, name=activity_text),
|
| 664 |
+
)
|
| 665 |
+
await self.bot.db.execute(
|
| 666 |
+
"INSERT INTO bot_presence_config(id, status, activity_type, activity_text, updated_by, updated_at) "
|
| 667 |
+
"VALUES (1, ?, ?, ?, ?, ?) "
|
| 668 |
+
"ON CONFLICT(id) DO UPDATE SET "
|
| 669 |
+
"status = excluded.status, "
|
| 670 |
+
"activity_type = excluded.activity_type, "
|
| 671 |
+
"activity_text = excluded.activity_text, "
|
| 672 |
+
"updated_by = excluded.updated_by, "
|
| 673 |
+
"updated_at = excluded.updated_at",
|
| 674 |
+
status_name,
|
| 675 |
+
activity_type_name,
|
| 676 |
+
activity_text,
|
| 677 |
+
actor_id,
|
| 678 |
+
dt.datetime.utcnow().isoformat(),
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
async def _presence_embed(self) -> discord.Embed:
|
| 682 |
+
current_activity = getattr(self.bot, "activity", None)
|
| 683 |
+
activity_text = getattr(current_activity, "name", None) or "CYBER // GRID"
|
| 684 |
+
activity_type_name = self._presence_activity_label(getattr(current_activity, "type", None))
|
| 685 |
+
status_name = self._presence_status_label(getattr(self.bot, "status", None))
|
| 686 |
+
row = await self.bot.db.fetchone(
|
| 687 |
+
"SELECT updated_by, updated_at FROM bot_presence_config WHERE id = 1"
|
| 688 |
+
)
|
| 689 |
+
updated_by = f"<@{row[0]}>" if row and row[0] else "Unknown"
|
| 690 |
+
updated_at = row[1] if row and row[1] else "Not saved yet"
|
| 691 |
+
embed = info_embed(
|
| 692 |
+
"Bot Presence",
|
| 693 |
+
(
|
| 694 |
+
f"Status: **{status_name}**\n"
|
| 695 |
+
f"Activity: **{activity_type_name}**\n"
|
| 696 |
+
f"Text: **{activity_text}**\n\n"
|
| 697 |
+
f"Updated by: {updated_by}\n"
|
| 698 |
+
f"Updated at: `{updated_at}`"
|
| 699 |
+
),
|
| 700 |
+
)
|
| 701 |
+
return embed
|
| 702 |
+
|
| 703 |
+
@commands.hybrid_command(name="purge", description=get_cmd_desc("commands.admin.purge_desc"))
|
| 704 |
@commands.has_permissions(manage_messages=True)
|
| 705 |
async def purge(self, ctx: commands.Context, amount: int) -> None:
|
| 706 |
"""Delete a number of messages from the channel."""
|
|
|
|
| 720 |
embed = success_embed("🗑️ Messages Purged", f"{panel_divider('green')}\n{desc}\n{panel_divider('green')}")
|
| 721 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction), delete_after=5 if not ctx.interaction else None)
|
| 722 |
|
| 723 |
+
@commands.hybrid_command(name="admin_panel", description=get_cmd_desc("commands.admin.admin_panel_desc"), with_app_command=False)
|
| 724 |
@commands.has_permissions(administrator=True)
|
| 725 |
async def admin_panel(self, ctx: commands.Context) -> None:
|
| 726 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
|
|
| 732 |
ephemeral=bool(ctx.interaction),
|
| 733 |
)
|
| 734 |
|
| 735 |
+
@commands.hybrid_group(name="admin", fallback="panel", description="Administrative grouped controls")
|
| 736 |
+
@commands.has_permissions(administrator=True)
|
| 737 |
+
async def admin_group(self, ctx: commands.Context) -> None:
|
| 738 |
+
await self.admin_panel(ctx)
|
| 739 |
+
|
| 740 |
+
@admin_group.group(name="botstatus", invoke_without_command=True)
|
| 741 |
+
@commands.is_owner()
|
| 742 |
+
async def admin_botstatus_group(self, ctx: commands.Context) -> None:
|
| 743 |
+
embed = await self._presence_embed()
|
| 744 |
+
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 745 |
+
|
| 746 |
+
@admin_botstatus_group.command(name="set")
|
| 747 |
+
@commands.is_owner()
|
| 748 |
+
async def admin_botstatus_set(
|
| 749 |
+
self,
|
| 750 |
+
ctx: commands.Context,
|
| 751 |
+
status: str,
|
| 752 |
+
activity_type: str,
|
| 753 |
+
*,
|
| 754 |
+
text: str,
|
| 755 |
+
) -> None:
|
| 756 |
+
status_name = (status or "").strip().lower()
|
| 757 |
+
activity_type_name = (activity_type or "").strip().lower()
|
| 758 |
+
activity_text = (text or "").strip()[:128]
|
| 759 |
+
|
| 760 |
+
if status_name not in PRESENCE_STATUS_MAP:
|
| 761 |
+
await self._safe_ctx_send(
|
| 762 |
+
ctx,
|
| 763 |
+
content="Invalid status. Use: `online`, `idle`, `dnd`, `invisible`",
|
| 764 |
+
ephemeral=bool(ctx.interaction),
|
| 765 |
+
)
|
| 766 |
+
return
|
| 767 |
+
if activity_type_name not in PRESENCE_ACTIVITY_MAP:
|
| 768 |
+
await self._safe_ctx_send(
|
| 769 |
+
ctx,
|
| 770 |
+
content="Invalid activity type. Use: `playing`, `watching`, `listening`, `competing`",
|
| 771 |
+
ephemeral=bool(ctx.interaction),
|
| 772 |
+
)
|
| 773 |
+
return
|
| 774 |
+
if not activity_text:
|
| 775 |
+
await self._safe_ctx_send(ctx, content="Activity text cannot be empty.", ephemeral=bool(ctx.interaction))
|
| 776 |
+
return
|
| 777 |
+
|
| 778 |
+
await self._set_bot_presence(
|
| 779 |
+
ctx.author.id,
|
| 780 |
+
status_name=status_name,
|
| 781 |
+
activity_type_name=activity_type_name,
|
| 782 |
+
activity_text=activity_text,
|
| 783 |
+
)
|
| 784 |
+
embed = success_embed(
|
| 785 |
+
"Bot Status Updated",
|
| 786 |
+
(
|
| 787 |
+
f"Status: **{status_name}**\n"
|
| 788 |
+
f"Activity: **{activity_type_name}**\n"
|
| 789 |
+
f"Text: **{activity_text}**"
|
| 790 |
+
),
|
| 791 |
+
)
|
| 792 |
+
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 793 |
+
|
| 794 |
+
@admin_botstatus_group.command(name="reset")
|
| 795 |
+
@commands.is_owner()
|
| 796 |
+
async def admin_botstatus_reset(self, ctx: commands.Context) -> None:
|
| 797 |
+
await self._set_bot_presence(
|
| 798 |
+
ctx.author.id,
|
| 799 |
+
status_name="online",
|
| 800 |
+
activity_type_name="playing",
|
| 801 |
+
activity_text="CYBER // GRID",
|
| 802 |
+
)
|
| 803 |
+
embed = success_embed("Bot Status Reset", "Restored default presence.")
|
| 804 |
+
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 805 |
+
|
| 806 |
+
@commands.hybrid_command(name="bot_status", description=get_cmd_desc("commands.admin.bot_status_desc"), with_app_command=True)
|
| 807 |
+
@commands.is_owner()
|
| 808 |
+
async def show_bot_status(self, ctx: commands.Context) -> None:
|
| 809 |
+
embed = await self._presence_embed()
|
| 810 |
+
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 811 |
+
|
| 812 |
+
@commands.hybrid_command(name="set_bot_status", description=get_cmd_desc("commands.admin.set_bot_status_desc"), with_app_command=True)
|
| 813 |
+
@commands.is_owner()
|
| 814 |
+
async def update_bot_status(
|
| 815 |
+
self,
|
| 816 |
+
ctx: commands.Context,
|
| 817 |
+
status: str,
|
| 818 |
+
activity_type: str,
|
| 819 |
+
*,
|
| 820 |
+
text: str,
|
| 821 |
+
) -> None:
|
| 822 |
+
await self.admin_botstatus_set(ctx, status, activity_type, text=text)
|
| 823 |
+
|
| 824 |
+
@commands.hybrid_command(name="reset_bot_status", description=get_cmd_desc("commands.admin.reset_bot_status_desc"), with_app_command=True)
|
| 825 |
+
@commands.is_owner()
|
| 826 |
+
async def restore_bot_status(self, ctx: commands.Context) -> None:
|
| 827 |
+
await self.admin_botstatus_reset(ctx)
|
| 828 |
+
|
| 829 |
+
@admin_group.group(name="shield", invoke_without_command=True)
|
| 830 |
+
@commands.has_permissions(administrator=True)
|
| 831 |
+
async def admin_shield_group(self, ctx: commands.Context) -> None:
|
| 832 |
await self._safe_ctx_send(
|
| 833 |
ctx,
|
| 834 |
content="Use `/admin shield state`, `/admin shield set_level <low|medium|high>`, or `/admin shield add_image <attachment>`",
|
|
|
|
| 863 |
)
|
| 864 |
await self._safe_ctx_send(ctx, content="✅ Scam image signature saved.", ephemeral=bool(ctx.interaction))
|
| 865 |
|
| 866 |
+
@commands.hybrid_command(name="shield_level", description=get_cmd_desc("commands.admin.shield_level_desc"), hidden=True, with_app_command=False)
|
| 867 |
@commands.has_permissions(administrator=True)
|
| 868 |
async def shield_level(self, ctx: commands.Context, level: str) -> None:
|
| 869 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
|
|
| 879 |
)
|
| 880 |
await self._safe_ctx_send(ctx, content=f"🛡️ Shield level set to `{normalized}`", ephemeral=bool(ctx.interaction))
|
| 881 |
|
| 882 |
+
@commands.hybrid_command(name="shield_state", description=get_cmd_desc("commands.admin.shield_state_desc"), with_app_command=True)
|
| 883 |
@commands.has_permissions(administrator=True)
|
| 884 |
async def shield_state(self, ctx: commands.Context) -> None:
|
| 885 |
if not ctx.guild:
|
|
|
|
| 908 |
)
|
| 909 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 910 |
|
| 911 |
+
@commands.hybrid_command(name="econ_admin", description=get_cmd_desc("commands.admin.econ_admin_desc"))
|
| 912 |
@commands.has_permissions(administrator=True)
|
| 913 |
async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None:
|
| 914 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
|
|
| 930 |
)
|
| 931 |
await self._safe_ctx_send(ctx, content=f"💰 {member.mention} coins: `{coins}` → `{new_coins}`", ephemeral=bool(ctx.interaction))
|
| 932 |
|
| 933 |
+
@commands.hybrid_command(name="warn", description=get_cmd_desc("commands.admin.warn_desc"))
|
| 934 |
@commands.has_permissions(manage_roles=True)
|
| 935 |
async def warn(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 936 |
"""Warn a member."""
|
|
|
|
| 978 |
except discord.Forbidden:
|
| 979 |
pass
|
| 980 |
|
| 981 |
+
@commands.hybrid_command(name="warnings", description=get_cmd_desc("commands.admin.warnings_desc"))
|
| 982 |
@commands.has_permissions(manage_roles=True)
|
| 983 |
async def warnings(self, ctx: commands.Context, member: discord.Member) -> None:
|
| 984 |
"""View warnings for a member."""
|
|
|
|
| 1013 |
embed.set_thumbnail(url=member.display_avatar.url)
|
| 1014 |
await ctx.reply(embed=embed)
|
| 1015 |
|
| 1016 |
+
@commands.hybrid_command(name="clearwarn", description=get_cmd_desc("commands.admin.clearwarn_desc"))
|
| 1017 |
@commands.has_permissions(manage_roles=True)
|
| 1018 |
async def clearwarn(self, ctx: commands.Context, member: discord.Member) -> None:
|
| 1019 |
"""Clear all warnings for a member."""
|
|
|
|
| 1032 |
embed = success_embed("✅ Warnings Cleared", f"🗑️ All warnings for {member.mention} have been removed.")
|
| 1033 |
await ctx.reply(embed=embed)
|
| 1034 |
|
| 1035 |
+
@commands.hybrid_command(name="kick", description=get_cmd_desc("commands.admin.kick_desc"))
|
| 1036 |
@commands.has_permissions(kick_members=True)
|
| 1037 |
async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1038 |
"""Kick a member from the server."""
|
|
|
|
| 1077 |
)
|
| 1078 |
await ctx.reply(embed=embed)
|
| 1079 |
|
| 1080 |
+
@commands.hybrid_command(name="ban", description=get_cmd_desc("commands.admin.ban_desc"))
|
| 1081 |
@commands.has_permissions(ban_members=True)
|
| 1082 |
async def ban(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1083 |
"""Ban a member from the server."""
|
|
|
|
| 1128 |
color=discord.Color.red(),
|
| 1129 |
)
|
| 1130 |
|
| 1131 |
+
@commands.hybrid_command(name="unban", description=get_cmd_desc("commands.admin.unban_desc"))
|
| 1132 |
@commands.has_permissions(ban_members=True)
|
| 1133 |
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = "No reason provided") -> None:
|
| 1134 |
"""Unban a user by their ID."""
|
|
|
|
| 1186 |
color=discord.Color.green(),
|
| 1187 |
)
|
| 1188 |
|
| 1189 |
+
@commands.hybrid_command(name="mute", description=get_cmd_desc("commands.admin.mute_desc"))
|
| 1190 |
@commands.has_permissions(moderate_members=True)
|
| 1191 |
async def mute(self, ctx: commands.Context, member: discord.Member, duration: int = 10, *, reason: str = "No reason provided") -> None:
|
| 1192 |
"""Timeout a member for a specified duration in minutes."""
|
|
|
|
| 1233 |
color=discord.Color.red(),
|
| 1234 |
)
|
| 1235 |
|
| 1236 |
+
@commands.hybrid_command(name="unmute", description=get_cmd_desc("commands.admin.unmute_desc"))
|
| 1237 |
@commands.has_permissions(moderate_members=True)
|
| 1238 |
async def unmute(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None:
|
| 1239 |
"""Remove timeout from a member."""
|
|
|
|
| 1328 |
if not ctx.guild:
|
| 1329 |
await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction))
|
| 1330 |
return
|
| 1331 |
+
embed = info_embed(
|
| 1332 |
+
"Emoji Clone Panel",
|
| 1333 |
+
"Quick actions:\n- Clone from tag/ID/URL/name\n- View server emojis\n- Open bot emoji picker",
|
| 1334 |
+
)
|
| 1335 |
await self._safe_ctx_send(ctx, embed=embed, view=EmojiClonePanelView(self), ephemeral=bool(ctx.interaction))
|
| 1336 |
|
| 1337 |
+
@commands.hybrid_command(name="emoji_clone_panel", description=get_cmd_desc("commands.admin.emoji_clone_panel_desc"), with_app_command=True)
|
| 1338 |
@commands.has_permissions(manage_emojis=True)
|
| 1339 |
async def emoji_clone_panel(self, ctx: commands.Context) -> None:
|
| 1340 |
await self.admin_emoji_panel(ctx)
|
|
|
|
| 1385 |
)
|
| 1386 |
await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
|
| 1387 |
|
| 1388 |
+
@commands.hybrid_command(name="awesomeroles", description=get_cmd_desc("commands.admin.awesomeroles_desc"))
|
| 1389 |
@commands.has_permissions(manage_roles=True)
|
| 1390 |
async def awesomeroles(self, ctx: commands.Context) -> None:
|
| 1391 |
"""Create awesome roles for the server with proper permissions."""
|
|
|
|
| 1450 |
embed.set_footer(text=f"{E_STAR} Created by {ctx.author.display_name}")
|
| 1451 |
await ctx.reply(embed=embed)
|
| 1452 |
|
| 1453 |
+
@commands.hybrid_command(name="backupserver", description=get_cmd_desc("commands.admin.backupserver_desc"))
|
| 1454 |
@commands.has_permissions(administrator=True)
|
| 1455 |
async def backupserver(self, ctx: commands.Context) -> None:
|
| 1456 |
"""Create a backup of server structure."""
|
|
|
|
| 1524 |
)
|
| 1525 |
await ctx.reply(embed=embed)
|
| 1526 |
|
| 1527 |
+
@commands.hybrid_command(name="backup_panel", description=get_cmd_desc("commands.admin.backup_panel_desc"))
|
| 1528 |
@commands.has_permissions(administrator=True)
|
| 1529 |
async def backup_panel(self, ctx: commands.Context) -> None:
|
| 1530 |
"""Open the interactive backup management panel."""
|
|
|
|
| 1765 |
pass
|
| 1766 |
|
| 1767 |
|
| 1768 |
+
@commands.hybrid_command(name="slowmode", description=get_cmd_desc("commands.admin.slowmode_desc"))
|
| 1769 |
@commands.has_permissions(manage_channels=True)
|
| 1770 |
async def slowmode(self, ctx: commands.Context, seconds: int = 0) -> None:
|
| 1771 |
"""Set slowmode for the current channel."""
|
|
|
|
| 1787 |
embed = success_embed("⏱️ Slowmode Disabled", f"📺 **Channel:** {ctx.channel.mention}")
|
| 1788 |
await ctx.reply(embed=embed)
|
| 1789 |
|
| 1790 |
+
@commands.hybrid_command(name="lock", description=get_cmd_desc("commands.admin.lock_desc"))
|
| 1791 |
@commands.has_permissions(manage_channels=True)
|
| 1792 |
async def lock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None:
|
| 1793 |
"""Lock a channel to prevent messages."""
|
|
|
|
| 1805 |
embed = success_embed("🔒 Channel Locked", f"📺 **Channel:** {channel.mention}")
|
| 1806 |
await ctx.reply(embed=embed)
|
| 1807 |
|
| 1808 |
+
@commands.hybrid_command(name="unlock", description=get_cmd_desc("commands.admin.unlock_desc"))
|
| 1809 |
@commands.has_permissions(manage_channels=True)
|
| 1810 |
async def unlock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None:
|
| 1811 |
"""Unlock a channel to allow messages."""
|
bot/cogs/ai_admin.py
CHANGED
|
@@ -3,15 +3,16 @@ Autonomous AI Administrator - Senior Backend Engineer Implementation
|
|
| 3 |
Permission Guard + Intelligence Layer + Execution Engine + Global Language Support
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
from __future__ import annotations
|
| 7 |
-
|
| 8 |
-
import datetime as dt
|
| 9 |
-
import json
|
| 10 |
-
import
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
import discord
|
| 14 |
-
from discord.ext import commands
|
| 15 |
|
| 16 |
try:
|
| 17 |
import aiohttp
|
|
@@ -59,177 +60,246 @@ class IntelligenceLayer:
|
|
| 59 |
api_key = getattr(settings, "openrouter_api_key", None)
|
| 60 |
if not api_key:
|
| 61 |
return None
|
| 62 |
-
|
| 63 |
model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini"
|
| 64 |
-
|
| 65 |
-
system_message = """You are an Autonomous AI Administrator for a Discord server.
|
| 66 |
-
Output ONLY a valid JSON array of actions. Do not include any other text.
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
|
|
|
| 71 |
{
|
| 72 |
"action": "create_role",
|
| 73 |
"name": "Role Name",
|
| 74 |
-
"color": "#
|
| 75 |
"hoist": true,
|
| 76 |
-
"
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
-
2.
|
| 80 |
{
|
| 81 |
"action": "create_channel",
|
| 82 |
-
"name": "
|
| 83 |
"type": "text",
|
| 84 |
-
"category": "
|
| 85 |
-
"
|
| 86 |
-
"
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
|
| 89 |
-
3.
|
| 90 |
{
|
| 91 |
-
"action": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
"channel": "channel-name",
|
| 93 |
-
"
|
| 94 |
-
"
|
| 95 |
-
"
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
{
|
| 100 |
"action": "create_giveaway",
|
| 101 |
-
"prize": "
|
| 102 |
-
"duration_minutes":
|
| 103 |
"winners": 1,
|
| 104 |
-
"channel": "
|
| 105 |
}
|
| 106 |
|
| 107 |
-
|
| 108 |
{
|
| 109 |
"action": "create_tournament",
|
| 110 |
-
"name": "
|
| 111 |
-
"game": "
|
| 112 |
"max_participants": 16,
|
| 113 |
-
"channel": "
|
| 114 |
}
|
| 115 |
|
| 116 |
-
|
| 117 |
{
|
| 118 |
"action": "create_poll",
|
| 119 |
-
"question": "
|
| 120 |
-
"options": ["
|
| 121 |
-
"duration_minutes":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
-
|
| 125 |
-
{
|
| 126 |
-
"action": "
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
"
|
| 135 |
-
"
|
| 136 |
-
"
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
"
|
| 143 |
-
"
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
"
|
| 159 |
-
"
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
"
|
| 166 |
-
"
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
"reason": "Why channel is renamed"
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
18. CREATE CATEGORY:
|
| 208 |
-
{
|
| 209 |
-
"action": "create_category",
|
| 210 |
-
"name": "Category Name",
|
| 211 |
-
"reason": "Why this category is needed"
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
19. RENAME CATEGORY:
|
| 215 |
-
{
|
| 216 |
-
"action": "rename_category",
|
| 217 |
-
"category": "Old Category Name",
|
| 218 |
-
"new_name": "New Category Name",
|
| 219 |
-
"reason": "Why category is renamed"
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
20. DELETE CATEGORY:
|
| 223 |
-
{
|
| 224 |
-
"action": "delete_category",
|
| 225 |
-
"category": "Category Name",
|
| 226 |
-
"reason": "Why category is deleted"
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
ALWAYS include a "response_to_user" field in the SAME language as the input.
|
| 230 |
-
Example: If input is Arabic, response_to_user must be in Arabic.
|
| 231 |
-
|
| 232 |
-
IMPORTANT: For any request that involves creating events, giveaways, tournaments, polls, or other bot features, use the appropriate action above. If unsure, use run_command."""
|
| 233 |
|
| 234 |
payload = {
|
| 235 |
"model": model,
|
|
@@ -238,15 +308,15 @@ IMPORTANT: For any request that involves creating events, giveaways, tournaments
|
|
| 238 |
{"role": "user", "content": prompt}
|
| 239 |
],
|
| 240 |
"temperature": 0.3,
|
| 241 |
-
"max_tokens":
|
| 242 |
}
|
| 243 |
-
|
| 244 |
headers = {
|
| 245 |
"Authorization": f"Bearer {api_key}",
|
| 246 |
"Content-Type": "application/json",
|
| 247 |
"HTTP-Referer": "https://github.com/mega-bot",
|
| 248 |
}
|
| 249 |
-
|
| 250 |
try:
|
| 251 |
session = await self._get_session()
|
| 252 |
async with session.post(
|
|
@@ -257,7 +327,7 @@ IMPORTANT: For any request that involves creating events, giveaways, tournaments
|
|
| 257 |
if resp.status != 200:
|
| 258 |
return None
|
| 259 |
data = await resp.json()
|
| 260 |
-
|
| 261 |
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 262 |
return self._parse_json_response(content)
|
| 263 |
except Exception:
|
|
@@ -288,6 +358,9 @@ class ExecutionEngine:
|
|
| 288 |
results = []
|
| 289 |
for action in actions:
|
| 290 |
action_type = action.get("action", "")
|
|
|
|
|
|
|
|
|
|
| 291 |
try:
|
| 292 |
if action_type == "create_role":
|
| 293 |
result = await self._create_role(action, ctx)
|
|
@@ -301,101 +374,107 @@ class ExecutionEngine:
|
|
| 301 |
result = await self._create_tournament(action, ctx)
|
| 302 |
elif action_type == "create_poll":
|
| 303 |
result = await self._create_poll(action, ctx)
|
| 304 |
-
elif action_type == "run_command":
|
| 305 |
-
result = await self._run_command(action, ctx)
|
| 306 |
-
elif action_type == "timeout_member":
|
| 307 |
-
result = await self._timeout_member(action, ctx)
|
| 308 |
-
elif action_type == "untimeout_member":
|
| 309 |
-
result = await self._untimeout_member(action, ctx)
|
| 310 |
-
elif action_type == "add_role":
|
| 311 |
-
result = await self._add_role(action, ctx)
|
| 312 |
-
elif action_type == "remove_role":
|
| 313 |
-
result = await self._remove_role(action, ctx)
|
| 314 |
-
elif action_type == "lock_channel":
|
| 315 |
-
result = await self._lock_channel(action, ctx)
|
| 316 |
-
elif action_type == "unlock_channel":
|
| 317 |
-
result = await self._unlock_channel(action, ctx)
|
| 318 |
-
elif action_type == "set_slowmode":
|
| 319 |
-
result = await self._set_slowmode(action, ctx)
|
| 320 |
-
elif action_type == "purge_messages":
|
| 321 |
-
result = await self._purge_messages(action, ctx)
|
| 322 |
-
elif action_type == "delete_channel":
|
| 323 |
-
result = await self._delete_channel(action, ctx)
|
| 324 |
-
elif action_type == "rename_channel":
|
| 325 |
-
result = await self._rename_channel(action, ctx)
|
| 326 |
-
elif action_type == "create_category":
|
| 327 |
-
result = await self._create_category(action, ctx)
|
| 328 |
-
elif action_type == "rename_category":
|
| 329 |
-
result = await self._rename_category(action, ctx)
|
| 330 |
-
elif action_type == "delete_category":
|
| 331 |
-
result = await self._delete_category(action, ctx)
|
| 332 |
-
|
| 333 |
-
result =
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
if
|
| 348 |
-
return
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
if
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
if
|
| 389 |
-
return
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 401 |
name = action.get("name", "New Role")
|
|
@@ -411,11 +490,11 @@ class ExecutionEngine:
|
|
| 411 |
role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason)
|
| 412 |
return f"Created role: {role.mention}"
|
| 413 |
|
| 414 |
-
async def _create_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 415 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 416 |
-
return "Manage Channels permission required."
|
| 417 |
-
name = action.get("name", "new-channel")
|
| 418 |
-
channel_type = action.get("type", "text")
|
| 419 |
category_name = action.get("category")
|
| 420 |
locked_roles = action.get("locked_to_roles", [])
|
| 421 |
reason = action.get("reason", "AI Admin")
|
|
@@ -489,214 +568,339 @@ class ExecutionEngine:
|
|
| 489 |
await cog.giveaway_create(ctx, int(duration), int(winners), prize=prize)
|
| 490 |
return f"Giveaway created: {prize}"
|
| 491 |
|
| 492 |
-
async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 493 |
-
name = action.get("name", "Tournament")
|
| 494 |
-
game = action.get("game", "Game")
|
| 495 |
-
max_participants = action.get("max_participants", 16)
|
| 496 |
-
|
| 497 |
-
cog = self.bot.get_cog("Engagement")
|
| 498 |
-
if not cog:
|
| 499 |
-
return "Engagement cog not found."
|
| 500 |
-
|
| 501 |
-
# Engagement exposes tournament creation via the base `/tournament` command.
|
| 502 |
-
games = f"{game}" if game else "chess, checkers, connect4, othello"
|
| 503 |
-
await ctx.invoke(cog.tournament, name=name, games=games)
|
| 504 |
-
return f"Tournament created: {name}"
|
| 505 |
-
|
| 506 |
-
async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 507 |
-
question = action.get("question", "Poll")
|
| 508 |
-
options = action.get("options", ["Yes", "No"])
|
| 509 |
-
|
| 510 |
-
cog = self.bot.get_cog("Utility")
|
| 511 |
-
if not cog:
|
| 512 |
-
return "Utility cog not found."
|
| 513 |
-
|
| 514 |
-
# Utility poll command accepts options as a "|" separated string.
|
| 515 |
-
options_str = "|".join(str(o) for o in options)
|
| 516 |
-
await ctx.invoke(cog.poll, question=question, options=options_str)
|
| 517 |
-
return f"Poll created: {question}"
|
| 518 |
|
| 519 |
-
async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 520 |
-
command_name = action.get("command", "")
|
| 521 |
-
args = action.get("args", {})
|
| 522 |
|
| 523 |
command = self.bot.get_command(command_name)
|
| 524 |
if not command:
|
| 525 |
return f"Command not found: {command_name}"
|
| 526 |
|
| 527 |
-
try:
|
| 528 |
-
await ctx.invoke(command, **args)
|
| 529 |
-
return f"Command executed: {command_name}"
|
| 530 |
-
except Exception as e:
|
| 531 |
-
return f"Error running command: {str(e)}"
|
| 532 |
-
|
| 533 |
-
async def _timeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 534 |
-
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 535 |
-
if not member:
|
| 536 |
-
return "Member not found."
|
| 537 |
-
minutes = max(1, min(int(action.get("minutes", 10)), 40320))
|
| 538 |
-
reason = action.get("reason", "AI Admin timeout")
|
| 539 |
-
until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
|
| 540 |
-
await member.timeout(until, reason=reason)
|
| 541 |
-
return f"Timed out {member.mention} for {minutes} minute(s)."
|
| 542 |
-
|
| 543 |
-
async def _untimeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 544 |
-
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 545 |
-
if not member:
|
| 546 |
-
return "Member not found."
|
| 547 |
-
reason = action.get("reason", "AI Admin untimeout")
|
| 548 |
-
await member.timeout(None, reason=reason)
|
| 549 |
-
return f"Removed timeout from {member.mention}."
|
| 550 |
-
|
| 551 |
-
async def _add_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 552 |
-
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 553 |
-
if not member:
|
| 554 |
-
return "Member not found."
|
| 555 |
-
role_name = str(action.get("role", "")).strip()
|
| 556 |
-
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 557 |
-
if not role:
|
| 558 |
-
return f"Role not found: {role_name}"
|
| 559 |
-
reason = action.get("reason", "AI Admin add role")
|
| 560 |
-
await member.add_roles(role, reason=reason)
|
| 561 |
-
return f"Added role **{role.name}** to {member.mention}."
|
| 562 |
-
|
| 563 |
-
async def _remove_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 564 |
-
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 565 |
-
if not member:
|
| 566 |
-
return "Member not found."
|
| 567 |
-
role_name = str(action.get("role", "")).strip()
|
| 568 |
-
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 569 |
-
if not role:
|
| 570 |
-
return f"Role not found: {role_name}"
|
| 571 |
-
reason = action.get("reason", "AI Admin remove role")
|
| 572 |
-
await member.remove_roles(role, reason=reason)
|
| 573 |
-
return f"Removed role **{role.name}** from {member.mention}."
|
| 574 |
-
|
| 575 |
-
async def _lock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 576 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 577 |
-
return "Manage Channels permission required."
|
| 578 |
-
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 579 |
-
if not channel:
|
| 580 |
-
return "Channel not found."
|
| 581 |
-
reason = action.get("reason", "AI Admin lock channel")
|
| 582 |
-
await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=reason)
|
| 583 |
-
return f"Locked channel {channel.mention}."
|
| 584 |
-
|
| 585 |
-
async def _unlock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 586 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 587 |
-
return "Manage Channels permission required."
|
| 588 |
-
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 589 |
-
if not channel:
|
| 590 |
-
return "Channel not found."
|
| 591 |
-
reason = action.get("reason", "AI Admin unlock channel")
|
| 592 |
-
await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=reason)
|
| 593 |
-
return f"Unlocked channel {channel.mention}."
|
| 594 |
-
|
| 595 |
-
async def _set_slowmode(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 596 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 597 |
-
return "Manage Channels permission required."
|
| 598 |
-
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 599 |
-
if not channel:
|
| 600 |
-
return "Channel not found."
|
| 601 |
-
seconds = max(0, min(int(action.get("seconds", 0)), 21600))
|
| 602 |
-
reason = action.get("reason", "AI Admin slowmode")
|
| 603 |
-
await channel.edit(slowmode_delay=seconds, reason=reason)
|
| 604 |
-
return f"Set slowmode in {channel.mention} to {seconds}s."
|
| 605 |
-
|
| 606 |
-
async def _purge_messages(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 607 |
-
if not ctx.author.guild_permissions.manage_messages:
|
| 608 |
-
return "Manage Messages permission required."
|
| 609 |
-
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 610 |
-
if not channel:
|
| 611 |
-
return "Channel not found."
|
| 612 |
-
amount = max(1, min(int(action.get("amount", 10)), 200))
|
| 613 |
-
deleted = await channel.purge(limit=amount)
|
| 614 |
-
return f"Purged {len(deleted)} message(s) in {channel.mention}."
|
| 615 |
-
|
| 616 |
-
async def _delete_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 617 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 618 |
-
return "Manage Channels permission required."
|
| 619 |
-
channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
|
| 620 |
-
if not channel:
|
| 621 |
-
return "Channel not found."
|
| 622 |
-
if ctx.channel and channel.id == ctx.channel.id:
|
| 623 |
-
return "Refusing to delete the channel currently being used for command execution."
|
| 624 |
-
reason = action.get("reason", "AI Admin delete channel")
|
| 625 |
-
name = channel.name
|
| 626 |
-
await channel.delete(reason=reason)
|
| 627 |
-
return f"Deleted channel #{name}."
|
| 628 |
-
|
| 629 |
-
async def _rename_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 630 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 631 |
-
return "Manage Channels permission required."
|
| 632 |
-
channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
|
| 633 |
-
if not channel:
|
| 634 |
-
return "Channel not found."
|
| 635 |
-
new_name = str(action.get("new_name", "")).strip()
|
| 636 |
-
if not new_name:
|
| 637 |
-
return "New channel name is required."
|
| 638 |
-
reason = action.get("reason", "AI Admin rename channel")
|
| 639 |
-
old_name = channel.name
|
| 640 |
-
await channel.edit(name=new_name[:100], reason=reason)
|
| 641 |
-
return f"Renamed channel **{old_name}** -> **{new_name[:100]}**."
|
| 642 |
-
|
| 643 |
-
async def _create_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 644 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 645 |
-
return "Manage Channels permission required."
|
| 646 |
-
name = str(action.get("name", "new-category")).strip()[:100]
|
| 647 |
-
if not name:
|
| 648 |
-
return "Category name is required."
|
| 649 |
-
reason = action.get("reason", "AI Admin create category")
|
| 650 |
-
category = await ctx.guild.create_category(name=name, reason=reason)
|
| 651 |
-
return f"Created category: **{category.name}**."
|
| 652 |
-
|
| 653 |
-
async def _rename_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 654 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 655 |
-
return "Manage Channels permission required."
|
| 656 |
-
category = self._resolve_category(ctx.guild, action.get("category"))
|
| 657 |
-
if not category:
|
| 658 |
-
return "Category not found."
|
| 659 |
-
new_name = str(action.get("new_name", "")).strip()[:100]
|
| 660 |
-
if not new_name:
|
| 661 |
-
return "New category name is required."
|
| 662 |
-
reason = action.get("reason", "AI Admin rename category")
|
| 663 |
-
old_name = category.name
|
| 664 |
-
await category.edit(name=new_name, reason=reason)
|
| 665 |
-
return f"Renamed category **{old_name}** -> **{new_name}**."
|
| 666 |
-
|
| 667 |
-
async def _delete_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 668 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 669 |
-
return "Manage Channels permission required."
|
| 670 |
-
category = self._resolve_category(ctx.guild, action.get("category"))
|
| 671 |
-
if not category:
|
| 672 |
-
return "Category not found."
|
| 673 |
-
reason = action.get("reason", "AI Admin delete category")
|
| 674 |
-
name = category.name
|
| 675 |
-
channels_inside = len(category.channels)
|
| 676 |
-
await category.delete(reason=reason)
|
| 677 |
-
return f"Deleted category **{name}** (had {channels_inside} channel(s))."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
|
| 679 |
-
class AIAdmin(commands.Cog):
|
| 680 |
"""Autonomous AI Administrator Cog."""
|
| 681 |
|
| 682 |
-
def __init__(self, bot: commands.Bot) -> None:
|
| 683 |
-
self.bot = bot
|
| 684 |
-
self.permission_guard = PermissionGuard()
|
| 685 |
-
self.intelligence = IntelligenceLayer(bot)
|
| 686 |
-
self.execution = ExecutionEngine(bot)
|
| 687 |
-
|
| 688 |
-
@staticmethod
|
| 689 |
-
def _parse_duration_minutes(text: str) -> int | None:
|
| 690 |
-
match = re.search(r"(\d+)\s*(m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE)
|
| 691 |
-
if not match:
|
| 692 |
-
return None
|
| 693 |
-
value = int(match.group(1))
|
| 694 |
-
unit = (match.group(2) or "m").lower()
|
| 695 |
-
if unit.startswith("h"):
|
| 696 |
-
value *= 60
|
| 697 |
-
elif unit.startswith("d"):
|
| 698 |
-
value *= 60 * 24
|
| 699 |
-
return max(1, min(value, 40320))
|
| 700 |
|
| 701 |
async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None:
|
| 702 |
if not ctx.guild:
|
|
@@ -712,34 +916,34 @@ class AIAdmin(commands.Cog):
|
|
| 712 |
if match:
|
| 713 |
target = ctx.guild.get_member(int(match.group(1)))
|
| 714 |
|
| 715 |
-
if lower.startswith("kick ") and target:
|
| 716 |
if not ctx.author.guild_permissions.kick_members:
|
| 717 |
return "You need Kick Members permission."
|
| 718 |
await target.kick(reason=f"AI Admin by {ctx.author}")
|
| 719 |
return f"OK: Kicked {target.mention}"
|
| 720 |
|
| 721 |
-
if lower.startswith("ban ") and target:
|
| 722 |
if not ctx.author.guild_permissions.ban_members:
|
| 723 |
return "You need Ban Members permission."
|
| 724 |
await target.ban(reason=f"AI Admin by {ctx.author}", delete_message_days=0)
|
| 725 |
return f"OK: Banned {target.mention}"
|
| 726 |
|
| 727 |
-
if (lower.startswith("mute ") or lower.startswith("timeout ")) and target:
|
| 728 |
-
if not ctx.author.guild_permissions.moderate_members:
|
| 729 |
-
return "You need Moderate Members permission."
|
| 730 |
-
minutes = self._parse_duration_minutes(text) or 10
|
| 731 |
-
until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
|
| 732 |
-
await target.timeout(until, reason=f"AI Admin by {ctx.author}")
|
| 733 |
-
return f"OK: Timed out {target.mention} for {minutes} minute(s)"
|
| 734 |
-
|
| 735 |
-
if (lower.startswith("unmute ") or lower.startswith("untimeout ")) and target:
|
| 736 |
-
if not ctx.author.guild_permissions.moderate_members:
|
| 737 |
-
return "You need Moderate Members permission."
|
| 738 |
-
await target.timeout(None, reason=f"AI Admin by {ctx.author}")
|
| 739 |
-
return f"OK: Removed timeout from {target.mention}"
|
| 740 |
-
|
| 741 |
-
role_match = re.search(r"(?:give|add)\s+role\s+(.+?)\s+(?:to|for)\s+<@!?(\d+)>", text, re.IGNORECASE)
|
| 742 |
-
if role_match:
|
| 743 |
role_name = role_match.group(1).strip(" \"'")
|
| 744 |
member = ctx.guild.get_member(int(role_match.group(2)))
|
| 745 |
if not member:
|
|
@@ -749,128 +953,166 @@ class AIAdmin(commands.Cog):
|
|
| 749 |
return f"Role not found: {role_name}"
|
| 750 |
if not ctx.author.guild_permissions.manage_roles:
|
| 751 |
return "You need Manage Roles permission."
|
| 752 |
-
await member.add_roles(role, reason=f"AI Admin by {ctx.author}")
|
| 753 |
-
return f"OK: Added role **{role.name}** to {member.mention}"
|
| 754 |
-
|
| 755 |
-
remove_role_match = re.search(r"(?:remove)\s+role\s+(.+?)\s+(?:from)\s+<@!?(\d+)>", text, re.IGNORECASE)
|
| 756 |
-
if remove_role_match:
|
| 757 |
-
role_name = remove_role_match.group(1).strip(" \"'")
|
| 758 |
-
member = ctx.guild.get_member(int(remove_role_match.group(2)))
|
| 759 |
-
if not member:
|
| 760 |
-
return "Target member not found."
|
| 761 |
-
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 762 |
-
if not role:
|
| 763 |
-
return f"Role not found: {role_name}"
|
| 764 |
-
if not ctx.author.guild_permissions.manage_roles:
|
| 765 |
-
return "You need Manage Roles permission."
|
| 766 |
-
await member.remove_roles(role, reason=f"AI Admin by {ctx.author}")
|
| 767 |
-
return f"OK: Removed role **{role.name}** from {member.mention}"
|
| 768 |
-
|
| 769 |
-
lock_match = re.search(r"^lock(?:\s+<#(\d+)>)?", lower)
|
| 770 |
-
if lock_match:
|
| 771 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 772 |
-
return "You need Manage Channels permission."
|
| 773 |
-
channel = ctx.guild.get_channel(int(lock_match.group(1))) if lock_match.group(1) else ctx.channel
|
| 774 |
-
if isinstance(channel, discord.TextChannel):
|
| 775 |
-
await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=f"AI Admin by {ctx.author}")
|
| 776 |
-
return f"OK: Locked {channel.mention}"
|
| 777 |
-
|
| 778 |
-
unlock_match = re.search(r"^unlock(?:\s+<#(\d+)>)?", lower)
|
| 779 |
-
if unlock_match:
|
| 780 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 781 |
-
return "You need Manage Channels permission."
|
| 782 |
-
channel = ctx.guild.get_channel(int(unlock_match.group(1))) if unlock_match.group(1) else ctx.channel
|
| 783 |
-
if isinstance(channel, discord.TextChannel):
|
| 784 |
-
await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=f"AI Admin by {ctx.author}")
|
| 785 |
-
return f"OK: Unlocked {channel.mention}"
|
| 786 |
-
|
| 787 |
-
rename_channel_match = re.search(
|
| 788 |
-
r"(?:rename)\s+channel\s+(.+?)\s+(?:to|->)\s+(.+)$",
|
| 789 |
-
text,
|
| 790 |
-
re.IGNORECASE,
|
| 791 |
-
)
|
| 792 |
-
if rename_channel_match:
|
| 793 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 794 |
-
return "You need Manage Channels permission."
|
| 795 |
-
old_ref = rename_channel_match.group(1).strip(" \"'")
|
| 796 |
-
new_name = rename_channel_match.group(2).strip(" \"'")
|
| 797 |
-
channel = ExecutionEngine._resolve_guild_channel(ctx.guild, old_ref)
|
| 798 |
-
if not channel:
|
| 799 |
-
return f"Channel not found: {old_ref}"
|
| 800 |
-
if not new_name:
|
| 801 |
-
return "New channel name is required."
|
| 802 |
-
old_name = channel.name
|
| 803 |
-
await channel.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
|
| 804 |
-
return f"OK: Renamed channel **{old_name}** -> **{new_name[:100]}**"
|
| 805 |
-
|
| 806 |
-
delete_channel_match = re.search(r"(?:delete|remove)\s+channel\s+(.+)$", text, re.IGNORECASE)
|
| 807 |
-
if delete_channel_match:
|
| 808 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 809 |
-
return "You need Manage Channels permission."
|
| 810 |
-
ref = delete_channel_match.group(1).strip(" \"'")
|
| 811 |
-
channel = ExecutionEngine._resolve_guild_channel(ctx.guild, ref)
|
| 812 |
-
if not channel:
|
| 813 |
-
return f"Channel not found: {ref}"
|
| 814 |
-
if ctx.channel and channel.id == ctx.channel.id:
|
| 815 |
-
return "Refusing to delete the channel currently being used for command execution."
|
| 816 |
-
name = channel.name
|
| 817 |
-
await channel.delete(reason=f"AI Admin by {ctx.author}")
|
| 818 |
-
return f"OK: Deleted channel **{name}**"
|
| 819 |
-
|
| 820 |
-
rename_category_match = re.search(
|
| 821 |
-
r"(?:rename)\s+(?:category|directory)\s+(.+?)\s+(?:to|->)\s+(.+)$",
|
| 822 |
-
text,
|
| 823 |
-
re.IGNORECASE,
|
| 824 |
-
)
|
| 825 |
-
if rename_category_match:
|
| 826 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 827 |
-
return "You need Manage Channels permission."
|
| 828 |
-
old_ref = rename_category_match.group(1).strip(" \"'")
|
| 829 |
-
new_name = rename_category_match.group(2).strip(" \"'")
|
| 830 |
-
category = ExecutionEngine._resolve_category(ctx.guild, old_ref)
|
| 831 |
-
if not category:
|
| 832 |
-
return f"Category not found: {old_ref}"
|
| 833 |
-
if not new_name:
|
| 834 |
-
return "New category name is required."
|
| 835 |
-
old_name = category.name
|
| 836 |
-
await category.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
|
| 837 |
-
return f"OK: Renamed category **{old_name}** -> **{new_name[:100]}**"
|
| 838 |
-
|
| 839 |
-
delete_category_match = re.search(r"(?:delete|remove)\s+(?:category|directory)\s+(.+)$", text, re.IGNORECASE)
|
| 840 |
-
if delete_category_match:
|
| 841 |
-
if not ctx.author.guild_permissions.manage_channels:
|
| 842 |
-
return "You need Manage Channels permission."
|
| 843 |
-
ref = delete_category_match.group(1).strip(" \"'")
|
| 844 |
-
category = ExecutionEngine._resolve_category(ctx.guild, ref)
|
| 845 |
-
if not category:
|
| 846 |
-
return f"Category not found: {ref}"
|
| 847 |
-
name = category.name
|
| 848 |
-
channels_inside = len(category.channels)
|
| 849 |
-
await category.delete(reason=f"AI Admin by {ctx.author}")
|
| 850 |
-
return f"OK: Deleted category **{name}** (had {channels_inside} channel(s))"
|
| 851 |
-
|
| 852 |
-
return None
|
| 853 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 854 |
async def cog_unload(self) -> None:
|
|
|
|
| 855 |
await self.intelligence.close()
|
| 856 |
|
| 857 |
-
@commands.hybrid_command(name="ai_admin", description="
|
| 858 |
-
async def ai_admin(self, ctx: commands.Context, *, request: str) -> None:
|
| 859 |
-
allowed, deny_reason = self.permission_guard.check(ctx)
|
| 860 |
-
if not allowed:
|
| 861 |
-
await ctx.send(deny_reason, ephemeral=True)
|
| 862 |
-
return
|
| 863 |
|
| 864 |
direct_result = await self._try_direct_moderation(ctx, request)
|
| 865 |
if direct_result is not None:
|
| 866 |
await ctx.send(direct_result, ephemeral=True)
|
| 867 |
return
|
| 868 |
|
| 869 |
-
if ctx.interaction and not ctx.interaction.response.is_done():
|
| 870 |
-
try:
|
| 871 |
-
await ctx.defer()
|
| 872 |
-
except discord.InteractionResponded:
|
| 873 |
-
pass
|
| 874 |
|
| 875 |
actions = await self.intelligence.ask_ai(request)
|
| 876 |
if not actions:
|
|
@@ -885,46 +1127,51 @@ class AIAdmin(commands.Cog):
|
|
| 885 |
|
| 886 |
results = await self.execution.execute(actions, ctx)
|
| 887 |
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
|
| 893 |
-
@commands.hybrid_command(name="ai_help", description="
|
| 894 |
async def ai_help(self, ctx: commands.Context) -> None:
|
| 895 |
embed = discord.Embed(
|
| 896 |
-
title="AI
|
| 897 |
description=(
|
| 898 |
-
"I
|
| 899 |
-
"**
|
| 900 |
-
"`/ai_admin
|
| 901 |
-
"
|
| 902 |
-
"
|
| 903 |
-
"
|
| 904 |
-
"
|
| 905 |
-
"
|
| 906 |
-
"`/ai_admin
|
| 907 |
-
"**
|
| 908 |
-
"`/ai_admin
|
| 909 |
-
"
|
| 910 |
-
"
|
| 911 |
-
"
|
| 912 |
-
"
|
| 913 |
-
"`/ai_admin ban @user`\n"
|
| 914 |
-
"`/ai_admin
|
| 915 |
-
"`/ai_admin
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
color=discord.Color.blue()
|
| 926 |
-
)
|
| 927 |
-
await ctx.send(embed=embed, ephemeral=True)
|
| 928 |
|
| 929 |
|
| 930 |
async def setup(bot: commands.Bot) -> None:
|
|
|
|
| 3 |
Permission Guard + Intelligence Layer + Execution Engine + Global Language Support
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import datetime as dt
|
| 9 |
+
import json
|
| 10 |
+
from bot.i18n import get_cmd_desc
|
| 11 |
+
import re
|
| 12 |
+
from typing import Any
|
| 13 |
|
| 14 |
import discord
|
| 15 |
+
from discord.ext import commands, tasks
|
| 16 |
|
| 17 |
try:
|
| 18 |
import aiohttp
|
|
|
|
| 60 |
api_key = getattr(settings, "openrouter_api_key", None)
|
| 61 |
if not api_key:
|
| 62 |
return None
|
| 63 |
+
|
| 64 |
model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini"
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
system_message = """You are a Human-like Discord Server Manager — an expert administrator who understands server structure, member behavior, and aesthetic organization.
|
| 67 |
+
|
| 68 |
+
You receive natural language requests (Arabic, English, or mixed) and output ONLY a valid JSON array of actions. Do NOT include any other text.
|
| 69 |
+
|
| 70 |
+
═══════════════════════════════════════════════════════
|
| 71 |
+
YOUR CAPABILITIES
|
| 72 |
+
═══════════════════════════════════════════════════════
|
| 73 |
|
| 74 |
+
━━━ SERVER ORCHESTRATION ━━━
|
| 75 |
+
1. CREATE_ROLE: Create a role with professional colors
|
| 76 |
{
|
| 77 |
"action": "create_role",
|
| 78 |
"name": "Role Name",
|
| 79 |
+
"color": "#800080",
|
| 80 |
"hoist": true,
|
| 81 |
+
"mentionable": false,
|
| 82 |
+
"reason": "Professional reason"
|
| 83 |
}
|
| 84 |
|
| 85 |
+
2. CREATE_CHANNEL: Create text/voice channels with emojis
|
| 86 |
{
|
| 87 |
"action": "create_channel",
|
| 88 |
+
"name": "💬-general-chat",
|
| 89 |
"type": "text",
|
| 90 |
+
"category": "Community",
|
| 91 |
+
"topic": "Channel topic",
|
| 92 |
+
"locked_to_roles": ["Staff"],
|
| 93 |
+
"deny_everyone": true,
|
| 94 |
+
"reason": "Why needed"
|
| 95 |
}
|
| 96 |
|
| 97 |
+
3. CREATE_CATEGORY: Organize server sections
|
| 98 |
{
|
| 99 |
+
"action": "create_category",
|
| 100 |
+
"name": "🏆 Competitions",
|
| 101 |
+
"reason": "Organize competition channels"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
4. PERMISSION_SETUP: Full permission architecture
|
| 105 |
+
{
|
| 106 |
+
"action": "setup_permissions",
|
| 107 |
"channel": "channel-name",
|
| 108 |
+
"deny_roles": ["@everyone", "Member"],
|
| 109 |
+
"allow_roles": ["Staff", "Admin"],
|
| 110 |
+
"deny_permissions": ["send_messages", "view_channel"],
|
| 111 |
+
"allow_permissions": ["send_messages", "view_channel", "manage_messages"],
|
| 112 |
+
"reason": "Staff-only room setup"
|
| 113 |
}
|
| 114 |
|
| 115 |
+
━━━ COMMUNITY FEATURES ━━━
|
| 116 |
+
5. ANNOUNCE: Rich embed announcement
|
| 117 |
+
{
|
| 118 |
+
"action": "announce",
|
| 119 |
+
"channel": "announcements",
|
| 120 |
+
"title": "📢 Important Update",
|
| 121 |
+
"description": "Announcement body",
|
| 122 |
+
"color": "#00FFFF",
|
| 123 |
+
"fields": [{"name": "Field 1", "value": "Value 1", "inline": true}],
|
| 124 |
+
"footer": "Footer text"
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
6. CREATE_GIVEAWAY: Feature setup
|
| 128 |
{
|
| 129 |
"action": "create_giveaway",
|
| 130 |
+
"prize": "Discord Nitro",
|
| 131 |
+
"duration_minutes": 1440,
|
| 132 |
"winners": 1,
|
| 133 |
+
"channel": "giveaways"
|
| 134 |
}
|
| 135 |
|
| 136 |
+
7. CREATE_TOURNAMENT: Tournament setup
|
| 137 |
{
|
| 138 |
"action": "create_tournament",
|
| 139 |
+
"name": "Valorant Cup",
|
| 140 |
+
"game": "Valorant",
|
| 141 |
"max_participants": 16,
|
| 142 |
+
"channel": "tournaments"
|
| 143 |
}
|
| 144 |
|
| 145 |
+
8. CREATE_POLL: Community poll
|
| 146 |
{
|
| 147 |
"action": "create_poll",
|
| 148 |
+
"question": "What game should we play?",
|
| 149 |
+
"options": ["Valorant", "Minecraft", "Fortnite"],
|
| 150 |
+
"duration_minutes": 60
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
━━━ MEMBER MANAGEMENT ━━━
|
| 154 |
+
9. TIMEOUT_MEMBER: Temporary timeout
|
| 155 |
+
{
|
| 156 |
+
"action": "timeout_member",
|
| 157 |
+
"member": "@user or user_id",
|
| 158 |
+
"minutes": 30,
|
| 159 |
+
"reason": "Rule violation"
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
10. UNTIMEOUT_MEMBER: Remove timeout
|
| 163 |
+
{
|
| 164 |
+
"action": "untimeout_member",
|
| 165 |
+
"member": "@user or user_id",
|
| 166 |
+
"reason": "Timeout removed"
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
11. ADD_ROLE: Assign role to member
|
| 170 |
+
{
|
| 171 |
+
"action": "add_role",
|
| 172 |
+
"member": "@user or user_id",
|
| 173 |
+
"role": "VIP",
|
| 174 |
+
"reason": "Reward for contribution"
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
12. REMOVE_ROLE: Remove role from member
|
| 178 |
+
{
|
| 179 |
+
"action": "remove_role",
|
| 180 |
+
"member": "@user or user_id",
|
| 181 |
+
"role": "Muted",
|
| 182 |
+
"reason": "Timeout period ended"
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
━━━ CHANNEL MANAGEMENT ━━━
|
| 186 |
+
13. LOCK_CHANNEL: Restrict channel
|
| 187 |
+
{
|
| 188 |
+
"action": "lock_channel",
|
| 189 |
+
"channel": "general",
|
| 190 |
+
"reason": "Raid prevention"
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
14. UNLOCK_CHANNEL: Restore access
|
| 194 |
+
{
|
| 195 |
+
"action": "unlock_channel",
|
| 196 |
+
"channel": "general",
|
| 197 |
+
"reason": "Situation resolved"
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
15. SET_SLOWMODE: Set delay
|
| 201 |
+
{
|
| 202 |
+
"action": "set_slowmode",
|
| 203 |
+
"channel": "general",
|
| 204 |
+
"seconds": 10,
|
| 205 |
+
"reason": "Reduce spam"
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
16. PURGE_MESSAGES: Clean channel
|
| 209 |
+
{
|
| 210 |
+
"action": "purge_messages",
|
| 211 |
+
"channel": "general",
|
| 212 |
+
"amount": 50,
|
| 213 |
+
"reason": "Spam cleanup"
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
17. DELETE_CHANNEL: Remove channel
|
| 217 |
+
{
|
| 218 |
+
"action": "delete_channel",
|
| 219 |
+
"channel": "old-channel",
|
| 220 |
+
"reason": "No longer needed"
|
| 221 |
}
|
| 222 |
|
| 223 |
+
18. RENAME_CHANNEL: Update name
|
| 224 |
+
{
|
| 225 |
+
"action": "rename_channel",
|
| 226 |
+
"channel": "old-name",
|
| 227 |
+
"new_name": "🎮-new-name",
|
| 228 |
+
"reason": "Better organization"
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
19. RENAME_CATEGORY: Update category
|
| 232 |
+
{
|
| 233 |
+
"action": "rename_category",
|
| 234 |
+
"category": "Old Category",
|
| 235 |
+
"new_name": "🎮 New Category",
|
| 236 |
+
"reason": "Aesthetic update"
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
20. DELETE_CATEGORY: Remove category
|
| 240 |
+
{
|
| 241 |
+
"action": "delete_category",
|
| 242 |
+
"category": "Unused Category",
|
| 243 |
+
"reason": "Cleanup"
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
━━━ SCHEDULING & ANALYSIS ━━━
|
| 247 |
+
21. SCHEDULE_TASK: Plan future action
|
| 248 |
+
{
|
| 249 |
+
"action": "schedule_task",
|
| 250 |
+
"run_at": "2025-01-15 21:00",
|
| 251 |
+
"action_to_run": {"action": "unlock_channel", "channel": "events", "reason": "Scheduled opening"},
|
| 252 |
+
"reason": "Auto-open events channel at 9 PM"
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
22. ANALYZE_ACTIVITY: Member activity report
|
| 256 |
+
{
|
| 257 |
+
"action": "analyze_activity",
|
| 258 |
+
"scope": "all" or "category-name" or "channel-name",
|
| 259 |
+
"period": "24h" or "7d" or "30d"
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
23. RUN_COMMAND: Fallback for anything else
|
| 263 |
+
{
|
| 264 |
+
"action": "run_command",
|
| 265 |
+
"command": "command_name",
|
| 266 |
+
"args": {"arg1": "value1"}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
═══════════════════════════════════════════════════════
|
| 270 |
+
YOUR BEHAVIOR RULES
|
| 271 |
+
═══════════════════════════════════════════════════════
|
| 272 |
+
|
| 273 |
+
🎨 AESTHETIC AUTONOMY:
|
| 274 |
+
- Use appropriate emojis in channel names (💬 chat, 🎮 gaming, 📢 announcements, 🎉 events)
|
| 275 |
+
- Choose professional hex colors: #1ABC9C (teal), #9B59B6 (purple), #E74C3C (red), #F1C40F (gold), #2ECC71 (green), #3498DB (blue)
|
| 276 |
+
- For staff roles: use purple (#9B59B6) or gold (#F1C40F)
|
| 277 |
+
- For member roles: use teal (#1ABC9C) or green (#2ECC71)
|
| 278 |
+
|
| 279 |
+
🏗️ STRATEGIC PLANNING:
|
| 280 |
+
- When asked to "setup X section" (e.g., "جهز قسم المسابقات"), plan multiple steps: create_category → create_roles → create_channels → setup_permissions
|
| 281 |
+
- Always think about the complete structure, not just one action
|
| 282 |
+
|
| 283 |
+
🔒 PERMISSION ARCHITECTURE:
|
| 284 |
+
- For "staff-only" or "private" channels: use setup_permissions with deny_everyone: true and allow_roles: ["Staff"]
|
| 285 |
+
- Understand hierarchy: @everyone should be denied, specific roles should be allowed
|
| 286 |
+
|
| 287 |
+
🗣️ HUMAN-LIKE RESPONSES:
|
| 288 |
+
- ALWAYS include "response_to_user" as the FIRST field in the FIRST action
|
| 289 |
+
- Match the user's language (Arabic → Arabic response, English → English)
|
| 290 |
+
- Be professional but conversational — explain your choices
|
| 291 |
+
- For Arabic: use administrative terms (تم إنشاء، تم تعيين، القسم جاهز)
|
| 292 |
+
|
| 293 |
+
⚠️ RISK ASSESSMENT:
|
| 294 |
+
- For destructive actions (delete, purge): mention impact in response_to_user
|
| 295 |
+
- Example: "سأحذف 50 رسالة من قناة general — هل أنت متأكد؟"
|
| 296 |
+
|
| 297 |
+
📝 MULTI-ACTION CHAINS:
|
| 298 |
+
- Return ALL actions needed in one response
|
| 299 |
+
- Example: "جهز قسم الألعاب" → create_category → create_role → create_channel (multiple) → setup_permissions
|
| 300 |
+
|
| 301 |
+
OUTPUT FORMAT: JSON array only. No markdown. No explanation outside the JSON.
|
| 302 |
+
ALWAYS start the FIRST object with "response_to_user" field."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
payload = {
|
| 305 |
"model": model,
|
|
|
|
| 308 |
{"role": "user", "content": prompt}
|
| 309 |
],
|
| 310 |
"temperature": 0.3,
|
| 311 |
+
"max_tokens": 3000
|
| 312 |
}
|
| 313 |
+
|
| 314 |
headers = {
|
| 315 |
"Authorization": f"Bearer {api_key}",
|
| 316 |
"Content-Type": "application/json",
|
| 317 |
"HTTP-Referer": "https://github.com/mega-bot",
|
| 318 |
}
|
| 319 |
+
|
| 320 |
try:
|
| 321 |
session = await self._get_session()
|
| 322 |
async with session.post(
|
|
|
|
| 327 |
if resp.status != 200:
|
| 328 |
return None
|
| 329 |
data = await resp.json()
|
| 330 |
+
|
| 331 |
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
| 332 |
return self._parse_json_response(content)
|
| 333 |
except Exception:
|
|
|
|
| 358 |
results = []
|
| 359 |
for action in actions:
|
| 360 |
action_type = action.get("action", "")
|
| 361 |
+
# Skip response_to_user — it's metadata, not an action
|
| 362 |
+
if action_type == "response_to_user":
|
| 363 |
+
continue
|
| 364 |
try:
|
| 365 |
if action_type == "create_role":
|
| 366 |
result = await self._create_role(action, ctx)
|
|
|
|
| 374 |
result = await self._create_tournament(action, ctx)
|
| 375 |
elif action_type == "create_poll":
|
| 376 |
result = await self._create_poll(action, ctx)
|
| 377 |
+
elif action_type == "run_command":
|
| 378 |
+
result = await self._run_command(action, ctx)
|
| 379 |
+
elif action_type == "timeout_member":
|
| 380 |
+
result = await self._timeout_member(action, ctx)
|
| 381 |
+
elif action_type == "untimeout_member":
|
| 382 |
+
result = await self._untimeout_member(action, ctx)
|
| 383 |
+
elif action_type == "add_role":
|
| 384 |
+
result = await self._add_role(action, ctx)
|
| 385 |
+
elif action_type == "remove_role":
|
| 386 |
+
result = await self._remove_role(action, ctx)
|
| 387 |
+
elif action_type == "lock_channel":
|
| 388 |
+
result = await self._lock_channel(action, ctx)
|
| 389 |
+
elif action_type == "unlock_channel":
|
| 390 |
+
result = await self._unlock_channel(action, ctx)
|
| 391 |
+
elif action_type == "set_slowmode":
|
| 392 |
+
result = await self._set_slowmode(action, ctx)
|
| 393 |
+
elif action_type == "purge_messages":
|
| 394 |
+
result = await self._purge_messages(action, ctx)
|
| 395 |
+
elif action_type == "delete_channel":
|
| 396 |
+
result = await self._delete_channel(action, ctx)
|
| 397 |
+
elif action_type == "rename_channel":
|
| 398 |
+
result = await self._rename_channel(action, ctx)
|
| 399 |
+
elif action_type == "create_category":
|
| 400 |
+
result = await self._create_category(action, ctx)
|
| 401 |
+
elif action_type == "rename_category":
|
| 402 |
+
result = await self._rename_category(action, ctx)
|
| 403 |
+
elif action_type == "delete_category":
|
| 404 |
+
result = await self._delete_category(action, ctx)
|
| 405 |
+
elif action_type == "setup_permissions":
|
| 406 |
+
result = await self._setup_permissions(action, ctx)
|
| 407 |
+
elif action_type == "schedule_task":
|
| 408 |
+
result = await self._schedule_task(action, ctx)
|
| 409 |
+
elif action_type == "analyze_activity":
|
| 410 |
+
result = await self._analyze_activity(action, ctx)
|
| 411 |
+
else:
|
| 412 |
+
result = f"Unknown action: {action_type}"
|
| 413 |
+
results.append(result)
|
| 414 |
+
except Exception as e:
|
| 415 |
+
results.append(f"Error executing {action_type}: {str(e)}")
|
| 416 |
+
return results
|
| 417 |
+
|
| 418 |
+
@staticmethod
|
| 419 |
+
def _resolve_member(guild: discord.Guild, ref: str | int | None) -> discord.Member | None:
|
| 420 |
+
if ref is None:
|
| 421 |
+
return None
|
| 422 |
+
text = str(ref).strip()
|
| 423 |
+
mention = re.search(r"<@!?(\d+)>", text)
|
| 424 |
+
if mention:
|
| 425 |
+
return guild.get_member(int(mention.group(1)))
|
| 426 |
+
if text.isdigit():
|
| 427 |
+
return guild.get_member(int(text))
|
| 428 |
+
return discord.utils.find(lambda m: m.name.lower() == text.lower() or m.display_name.lower() == text.lower(), guild.members)
|
| 429 |
+
|
| 430 |
+
@staticmethod
|
| 431 |
+
def _resolve_channel(guild: discord.Guild, ref: str | None, fallback: discord.abc.GuildChannel | None = None) -> discord.TextChannel | None:
|
| 432 |
+
if ref:
|
| 433 |
+
text = str(ref).strip()
|
| 434 |
+
mention = re.search(r"<#(\d+)>", text)
|
| 435 |
+
if mention:
|
| 436 |
+
ch = guild.get_channel(int(mention.group(1)))
|
| 437 |
+
return ch if isinstance(ch, discord.TextChannel) else None
|
| 438 |
+
by_name = discord.utils.get(guild.text_channels, name=text)
|
| 439 |
+
if by_name:
|
| 440 |
+
return by_name
|
| 441 |
+
if isinstance(fallback, discord.TextChannel):
|
| 442 |
+
return fallback
|
| 443 |
+
return None
|
| 444 |
+
|
| 445 |
+
@staticmethod
|
| 446 |
+
def _resolve_guild_channel(
|
| 447 |
+
guild: discord.Guild,
|
| 448 |
+
ref: str | int | None,
|
| 449 |
+
fallback: discord.abc.GuildChannel | None = None,
|
| 450 |
+
) -> discord.abc.GuildChannel | None:
|
| 451 |
+
if ref is not None:
|
| 452 |
+
text = str(ref).strip()
|
| 453 |
+
mention = re.search(r"<#(\d+)>", text)
|
| 454 |
+
if mention:
|
| 455 |
+
return guild.get_channel(int(mention.group(1)))
|
| 456 |
+
if text.isdigit():
|
| 457 |
+
return guild.get_channel(int(text))
|
| 458 |
+
by_name = discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.channels)
|
| 459 |
+
if by_name:
|
| 460 |
+
return by_name
|
| 461 |
+
if isinstance(fallback, discord.abc.GuildChannel):
|
| 462 |
+
return fallback
|
| 463 |
+
return None
|
| 464 |
+
|
| 465 |
+
@staticmethod
|
| 466 |
+
def _resolve_category(guild: discord.Guild, ref: str | int | None) -> discord.CategoryChannel | None:
|
| 467 |
+
if ref is None:
|
| 468 |
+
return None
|
| 469 |
+
text = str(ref).strip()
|
| 470 |
+
mention = re.search(r"<#(\d+)>", text)
|
| 471 |
+
if mention:
|
| 472 |
+
ch = guild.get_channel(int(mention.group(1)))
|
| 473 |
+
return ch if isinstance(ch, discord.CategoryChannel) else None
|
| 474 |
+
if text.isdigit():
|
| 475 |
+
ch = guild.get_channel(int(text))
|
| 476 |
+
return ch if isinstance(ch, discord.CategoryChannel) else None
|
| 477 |
+
return discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.categories)
|
| 478 |
|
| 479 |
async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 480 |
name = action.get("name", "New Role")
|
|
|
|
| 490 |
role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason)
|
| 491 |
return f"Created role: {role.mention}"
|
| 492 |
|
| 493 |
+
async def _create_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 494 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 495 |
+
return "Manage Channels permission required."
|
| 496 |
+
name = action.get("name", "new-channel")
|
| 497 |
+
channel_type = action.get("type", "text")
|
| 498 |
category_name = action.get("category")
|
| 499 |
locked_roles = action.get("locked_to_roles", [])
|
| 500 |
reason = action.get("reason", "AI Admin")
|
|
|
|
| 568 |
await cog.giveaway_create(ctx, int(duration), int(winners), prize=prize)
|
| 569 |
return f"Giveaway created: {prize}"
|
| 570 |
|
| 571 |
+
async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 572 |
+
name = action.get("name", "Tournament")
|
| 573 |
+
game = action.get("game", "Game")
|
| 574 |
+
max_participants = action.get("max_participants", 16)
|
| 575 |
+
|
| 576 |
+
cog = self.bot.get_cog("Engagement")
|
| 577 |
+
if not cog:
|
| 578 |
+
return "Engagement cog not found."
|
| 579 |
+
|
| 580 |
+
# Engagement exposes tournament creation via the base `/tournament` command.
|
| 581 |
+
games = f"{game}" if game else "chess, checkers, connect4, othello"
|
| 582 |
+
await ctx.invoke(cog.tournament, name=name, games=games)
|
| 583 |
+
return f"Tournament created: {name}"
|
| 584 |
+
|
| 585 |
+
async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 586 |
+
question = action.get("question", "Poll")
|
| 587 |
+
options = action.get("options", ["Yes", "No"])
|
| 588 |
+
|
| 589 |
+
cog = self.bot.get_cog("Utility")
|
| 590 |
+
if not cog:
|
| 591 |
+
return "Utility cog not found."
|
| 592 |
+
|
| 593 |
+
# Utility poll command accepts options as a "|" separated string.
|
| 594 |
+
options_str = "|".join(str(o) for o in options)
|
| 595 |
+
await ctx.invoke(cog.poll, question=question, options=options_str)
|
| 596 |
+
return f"Poll created: {question}"
|
| 597 |
|
| 598 |
+
async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 599 |
+
command_name = action.get("command", "")
|
| 600 |
+
args = action.get("args", {})
|
| 601 |
|
| 602 |
command = self.bot.get_command(command_name)
|
| 603 |
if not command:
|
| 604 |
return f"Command not found: {command_name}"
|
| 605 |
|
| 606 |
+
try:
|
| 607 |
+
await ctx.invoke(command, **args)
|
| 608 |
+
return f"Command executed: {command_name}"
|
| 609 |
+
except Exception as e:
|
| 610 |
+
return f"Error running command: {str(e)}"
|
| 611 |
+
|
| 612 |
+
async def _timeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 613 |
+
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 614 |
+
if not member:
|
| 615 |
+
return "Member not found."
|
| 616 |
+
minutes = max(1, min(int(action.get("minutes", 10)), 40320))
|
| 617 |
+
reason = action.get("reason", "AI Admin timeout")
|
| 618 |
+
until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
|
| 619 |
+
await member.timeout(until, reason=reason)
|
| 620 |
+
return f"Timed out {member.mention} for {minutes} minute(s)."
|
| 621 |
+
|
| 622 |
+
async def _untimeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 623 |
+
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 624 |
+
if not member:
|
| 625 |
+
return "Member not found."
|
| 626 |
+
reason = action.get("reason", "AI Admin untimeout")
|
| 627 |
+
await member.timeout(None, reason=reason)
|
| 628 |
+
return f"Removed timeout from {member.mention}."
|
| 629 |
+
|
| 630 |
+
async def _add_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 631 |
+
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 632 |
+
if not member:
|
| 633 |
+
return "Member not found."
|
| 634 |
+
role_name = str(action.get("role", "")).strip()
|
| 635 |
+
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 636 |
+
if not role:
|
| 637 |
+
return f"Role not found: {role_name}"
|
| 638 |
+
reason = action.get("reason", "AI Admin add role")
|
| 639 |
+
await member.add_roles(role, reason=reason)
|
| 640 |
+
return f"Added role **{role.name}** to {member.mention}."
|
| 641 |
+
|
| 642 |
+
async def _remove_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 643 |
+
member = self._resolve_member(ctx.guild, action.get("member"))
|
| 644 |
+
if not member:
|
| 645 |
+
return "Member not found."
|
| 646 |
+
role_name = str(action.get("role", "")).strip()
|
| 647 |
+
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 648 |
+
if not role:
|
| 649 |
+
return f"Role not found: {role_name}"
|
| 650 |
+
reason = action.get("reason", "AI Admin remove role")
|
| 651 |
+
await member.remove_roles(role, reason=reason)
|
| 652 |
+
return f"Removed role **{role.name}** from {member.mention}."
|
| 653 |
+
|
| 654 |
+
async def _lock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 655 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 656 |
+
return "Manage Channels permission required."
|
| 657 |
+
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 658 |
+
if not channel:
|
| 659 |
+
return "Channel not found."
|
| 660 |
+
reason = action.get("reason", "AI Admin lock channel")
|
| 661 |
+
await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=reason)
|
| 662 |
+
return f"Locked channel {channel.mention}."
|
| 663 |
+
|
| 664 |
+
async def _unlock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 665 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 666 |
+
return "Manage Channels permission required."
|
| 667 |
+
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 668 |
+
if not channel:
|
| 669 |
+
return "Channel not found."
|
| 670 |
+
reason = action.get("reason", "AI Admin unlock channel")
|
| 671 |
+
await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=reason)
|
| 672 |
+
return f"Unlocked channel {channel.mention}."
|
| 673 |
+
|
| 674 |
+
async def _set_slowmode(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 675 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 676 |
+
return "Manage Channels permission required."
|
| 677 |
+
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 678 |
+
if not channel:
|
| 679 |
+
return "Channel not found."
|
| 680 |
+
seconds = max(0, min(int(action.get("seconds", 0)), 21600))
|
| 681 |
+
reason = action.get("reason", "AI Admin slowmode")
|
| 682 |
+
await channel.edit(slowmode_delay=seconds, reason=reason)
|
| 683 |
+
return f"Set slowmode in {channel.mention} to {seconds}s."
|
| 684 |
+
|
| 685 |
+
async def _purge_messages(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 686 |
+
if not ctx.author.guild_permissions.manage_messages:
|
| 687 |
+
return "Manage Messages permission required."
|
| 688 |
+
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 689 |
+
if not channel:
|
| 690 |
+
return "Channel not found."
|
| 691 |
+
amount = max(1, min(int(action.get("amount", 10)), 200))
|
| 692 |
+
deleted = await channel.purge(limit=amount)
|
| 693 |
+
return f"Purged {len(deleted)} message(s) in {channel.mention}."
|
| 694 |
+
|
| 695 |
+
async def _delete_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 696 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 697 |
+
return "Manage Channels permission required."
|
| 698 |
+
channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
|
| 699 |
+
if not channel:
|
| 700 |
+
return "Channel not found."
|
| 701 |
+
if ctx.channel and channel.id == ctx.channel.id:
|
| 702 |
+
return "Refusing to delete the channel currently being used for command execution."
|
| 703 |
+
reason = action.get("reason", "AI Admin delete channel")
|
| 704 |
+
name = channel.name
|
| 705 |
+
await channel.delete(reason=reason)
|
| 706 |
+
return f"Deleted channel #{name}."
|
| 707 |
+
|
| 708 |
+
async def _rename_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 709 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 710 |
+
return "Manage Channels permission required."
|
| 711 |
+
channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
|
| 712 |
+
if not channel:
|
| 713 |
+
return "Channel not found."
|
| 714 |
+
new_name = str(action.get("new_name", "")).strip()
|
| 715 |
+
if not new_name:
|
| 716 |
+
return "New channel name is required."
|
| 717 |
+
reason = action.get("reason", "AI Admin rename channel")
|
| 718 |
+
old_name = channel.name
|
| 719 |
+
await channel.edit(name=new_name[:100], reason=reason)
|
| 720 |
+
return f"Renamed channel **{old_name}** -> **{new_name[:100]}**."
|
| 721 |
+
|
| 722 |
+
async def _create_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 723 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 724 |
+
return "Manage Channels permission required."
|
| 725 |
+
name = str(action.get("name", "new-category")).strip()[:100]
|
| 726 |
+
if not name:
|
| 727 |
+
return "Category name is required."
|
| 728 |
+
reason = action.get("reason", "AI Admin create category")
|
| 729 |
+
category = await ctx.guild.create_category(name=name, reason=reason)
|
| 730 |
+
return f"Created category: **{category.name}**."
|
| 731 |
+
|
| 732 |
+
async def _rename_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 733 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 734 |
+
return "Manage Channels permission required."
|
| 735 |
+
category = self._resolve_category(ctx.guild, action.get("category"))
|
| 736 |
+
if not category:
|
| 737 |
+
return "Category not found."
|
| 738 |
+
new_name = str(action.get("new_name", "")).strip()[:100]
|
| 739 |
+
if not new_name:
|
| 740 |
+
return "New category name is required."
|
| 741 |
+
reason = action.get("reason", "AI Admin rename category")
|
| 742 |
+
old_name = category.name
|
| 743 |
+
await category.edit(name=new_name, reason=reason)
|
| 744 |
+
return f"Renamed category **{old_name}** -> **{new_name}**."
|
| 745 |
+
|
| 746 |
+
async def _delete_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 747 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 748 |
+
return "Manage Channels permission required."
|
| 749 |
+
category = self._resolve_category(ctx.guild, action.get("category"))
|
| 750 |
+
if not category:
|
| 751 |
+
return "Category not found."
|
| 752 |
+
reason = action.get("reason", "AI Admin delete category")
|
| 753 |
+
name = category.name
|
| 754 |
+
channels_inside = len(category.channels)
|
| 755 |
+
await category.delete(reason=reason)
|
| 756 |
+
return f"Deleted category **{name}** (had {channels_inside} channel(s))."
|
| 757 |
+
|
| 758 |
+
async def _setup_permissions(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 759 |
+
"""Set up complex channel permissions for specific roles."""
|
| 760 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 761 |
+
return "Manage Channels permission required."
|
| 762 |
+
channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
|
| 763 |
+
if not channel:
|
| 764 |
+
return "Channel not found."
|
| 765 |
+
|
| 766 |
+
deny_roles = action.get("deny_roles", ["@everyone"])
|
| 767 |
+
allow_roles = action.get("allow_roles", [])
|
| 768 |
+
deny_perms = action.get("deny_permissions", ["send_messages"])
|
| 769 |
+
allow_perms = action.get("allow_permissions", ["send_messages"])
|
| 770 |
+
reason = action.get("reason", "AI Admin permission setup")
|
| 771 |
+
|
| 772 |
+
overwrites = {}
|
| 773 |
+
# Process denied roles
|
| 774 |
+
for role_name in deny_roles:
|
| 775 |
+
if role_name.lower() in ("@everyone", "everyone"):
|
| 776 |
+
role_obj = ctx.guild.default_role
|
| 777 |
+
else:
|
| 778 |
+
role_obj = discord.utils.get(ctx.guild.roles, name=role_name)
|
| 779 |
+
if role_obj:
|
| 780 |
+
deny_obj = discord.PermissionOverwrite()
|
| 781 |
+
for perm in deny_perms:
|
| 782 |
+
setattr(deny_obj, perm, False)
|
| 783 |
+
overwrites[role_obj] = deny_obj
|
| 784 |
+
|
| 785 |
+
# Process allowed roles
|
| 786 |
+
for role_name in allow_roles:
|
| 787 |
+
role_obj = discord.utils.get(ctx.guild.roles, name=role_name)
|
| 788 |
+
if role_obj:
|
| 789 |
+
allow_obj = discord.PermissionOverwrite()
|
| 790 |
+
for perm in allow_perms:
|
| 791 |
+
setattr(allow_obj, perm, True)
|
| 792 |
+
overwrites[role_obj] = allow_obj
|
| 793 |
+
|
| 794 |
+
await channel.edit(overwrites=overwrites, reason=reason)
|
| 795 |
+
denied = ", ".join(deny_roles)
|
| 796 |
+
allowed = ", ".join(allow_roles) if allow_roles else "none"
|
| 797 |
+
return f"Permissions set for **{channel.mention}**: denied [{denied}], allowed [{allowed}]."
|
| 798 |
+
|
| 799 |
+
async def _schedule_task(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 800 |
+
"""Schedule a task for future execution."""
|
| 801 |
+
run_at = action.get("run_at", "")
|
| 802 |
+
action_to_run = action.get("action_to_run", {})
|
| 803 |
+
reason = action.get("reason", "Scheduled task")
|
| 804 |
+
|
| 805 |
+
# Parse the datetime
|
| 806 |
+
try:
|
| 807 |
+
scheduled_time = dt.datetime.fromisoformat(run_at)
|
| 808 |
+
if scheduled_time.tzinfo is None:
|
| 809 |
+
scheduled_time = scheduled_time.replace(tzinfo=dt.timezone.utc)
|
| 810 |
+
except (ValueError, TypeError):
|
| 811 |
+
return f"Invalid schedule time: {run_at}. Use ISO format: YYYY-MM-DD HH:MM"
|
| 812 |
+
|
| 813 |
+
if scheduled_time <= dt.datetime.now(dt.timezone.utc):
|
| 814 |
+
return "Scheduled time must be in the future."
|
| 815 |
+
|
| 816 |
+
# Store in database
|
| 817 |
+
await self.bot.db.execute(
|
| 818 |
+
"INSERT INTO ai_scheduled_tasks(guild_id, run_at, action_json, reason, created_by) VALUES (?, ?, ?, ?, ?)",
|
| 819 |
+
ctx.guild.id,
|
| 820 |
+
scheduled_time.isoformat(),
|
| 821 |
+
json.dumps(action_to_run),
|
| 822 |
+
reason[:200],
|
| 823 |
+
ctx.author.id,
|
| 824 |
+
)
|
| 825 |
+
time_str = scheduled_time.strftime("%Y-%m-%d %H:%M UTC")
|
| 826 |
+
return f"⏰ Task scheduled for **{time_str}**: {reason[:100]}"
|
| 827 |
+
|
| 828 |
+
async def _analyze_activity(self, action: dict[str, Any], ctx: commands.Context) -> str:
|
| 829 |
+
"""Analyze member activity and return a summary."""
|
| 830 |
+
scope = action.get("scope", "all")
|
| 831 |
+
period = action.get("period", "24h")
|
| 832 |
+
|
| 833 |
+
# Parse period
|
| 834 |
+
hours = {"24h": 24, "7d": 168, "30d": 720}.get(period, 24)
|
| 835 |
+
|
| 836 |
+
# Get recent messages in scope
|
| 837 |
+
if scope == "all":
|
| 838 |
+
channels = ctx.guild.text_channels
|
| 839 |
+
else:
|
| 840 |
+
ch = discord.utils.get(ctx.guild.text_channels, name=scope.lower().replace(" ", "-"))
|
| 841 |
+
cat = discord.utils.get(ctx.guild.categories, name=scope)
|
| 842 |
+
if ch:
|
| 843 |
+
channels = [ch]
|
| 844 |
+
elif cat:
|
| 845 |
+
channels = cat.text_channels
|
| 846 |
+
else:
|
| 847 |
+
channels = ctx.guild.text_channels
|
| 848 |
+
|
| 849 |
+
member_counts: dict[int, int] = {}
|
| 850 |
+
total = 0
|
| 851 |
+
for ch in channels[:10]: # Limit to first 10 channels to avoid timeout
|
| 852 |
+
try:
|
| 853 |
+
async for msg in ch.history(limit=200):
|
| 854 |
+
if msg.author.bot:
|
| 855 |
+
continue
|
| 856 |
+
age = dt.datetime.now(dt.timezone.utc) - msg.created_at
|
| 857 |
+
if age.total_seconds() > hours * 3600:
|
| 858 |
+
continue
|
| 859 |
+
member_counts[msg.author.id] = member_counts.get(msg.author.id, 0) + 1
|
| 860 |
+
total += 1
|
| 861 |
+
except (discord.Forbidden, discord.HTTPException):
|
| 862 |
+
continue
|
| 863 |
+
|
| 864 |
+
if not member_counts:
|
| 865 |
+
return f"📊 **Activity Report ({period})**: No recent activity found."
|
| 866 |
+
|
| 867 |
+
# Top 5 active
|
| 868 |
+
sorted_members = sorted(member_counts.items(), key=lambda x: -x[1])[:5]
|
| 869 |
+
lines = []
|
| 870 |
+
for rank, (user_id, count) in enumerate(sorted_members, 1):
|
| 871 |
+
member = ctx.guild.get_member(user_id)
|
| 872 |
+
name = member.mention if member else f"<@{user_id}>"
|
| 873 |
+
medal = {1: "🥇", 2: "🥈", 3: "🥉"}.get(rank, f"{rank}.")
|
| 874 |
+
lines.append(f"{medal} {name}: `{count}` messages")
|
| 875 |
+
|
| 876 |
+
unique_users = len(member_counts)
|
| 877 |
+
return (
|
| 878 |
+
f"📊 **Activity Report ({period})**\n"
|
| 879 |
+
f"Total messages: `{total}` | Active users: `{unique_users}`\n\n"
|
| 880 |
+
f"**Top 5:**\n" + "\n".join(lines)
|
| 881 |
+
)
|
| 882 |
|
| 883 |
+
class AIAdmin(commands.Cog):
|
| 884 |
"""Autonomous AI Administrator Cog."""
|
| 885 |
|
| 886 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 887 |
+
self.bot = bot
|
| 888 |
+
self.permission_guard = PermissionGuard()
|
| 889 |
+
self.intelligence = IntelligenceLayer(bot)
|
| 890 |
+
self.execution = ExecutionEngine(bot)
|
| 891 |
+
|
| 892 |
+
@staticmethod
|
| 893 |
+
def _parse_duration_minutes(text: str) -> int | None:
|
| 894 |
+
match = re.search(r"(\d+)\s*(m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE)
|
| 895 |
+
if not match:
|
| 896 |
+
return None
|
| 897 |
+
value = int(match.group(1))
|
| 898 |
+
unit = (match.group(2) or "m").lower()
|
| 899 |
+
if unit.startswith("h"):
|
| 900 |
+
value *= 60
|
| 901 |
+
elif unit.startswith("d"):
|
| 902 |
+
value *= 60 * 24
|
| 903 |
+
return max(1, min(value, 40320))
|
| 904 |
|
| 905 |
async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None:
|
| 906 |
if not ctx.guild:
|
|
|
|
| 916 |
if match:
|
| 917 |
target = ctx.guild.get_member(int(match.group(1)))
|
| 918 |
|
| 919 |
+
if lower.startswith("kick ") and target:
|
| 920 |
if not ctx.author.guild_permissions.kick_members:
|
| 921 |
return "You need Kick Members permission."
|
| 922 |
await target.kick(reason=f"AI Admin by {ctx.author}")
|
| 923 |
return f"OK: Kicked {target.mention}"
|
| 924 |
|
| 925 |
+
if lower.startswith("ban ") and target:
|
| 926 |
if not ctx.author.guild_permissions.ban_members:
|
| 927 |
return "You need Ban Members permission."
|
| 928 |
await target.ban(reason=f"AI Admin by {ctx.author}", delete_message_days=0)
|
| 929 |
return f"OK: Banned {target.mention}"
|
| 930 |
|
| 931 |
+
if (lower.startswith("mute ") or lower.startswith("timeout ")) and target:
|
| 932 |
+
if not ctx.author.guild_permissions.moderate_members:
|
| 933 |
+
return "You need Moderate Members permission."
|
| 934 |
+
minutes = self._parse_duration_minutes(text) or 10
|
| 935 |
+
until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
|
| 936 |
+
await target.timeout(until, reason=f"AI Admin by {ctx.author}")
|
| 937 |
+
return f"OK: Timed out {target.mention} for {minutes} minute(s)"
|
| 938 |
+
|
| 939 |
+
if (lower.startswith("unmute ") or lower.startswith("untimeout ")) and target:
|
| 940 |
+
if not ctx.author.guild_permissions.moderate_members:
|
| 941 |
+
return "You need Moderate Members permission."
|
| 942 |
+
await target.timeout(None, reason=f"AI Admin by {ctx.author}")
|
| 943 |
+
return f"OK: Removed timeout from {target.mention}"
|
| 944 |
+
|
| 945 |
+
role_match = re.search(r"(?:give|add)\s+role\s+(.+?)\s+(?:to|for)\s+<@!?(\d+)>", text, re.IGNORECASE)
|
| 946 |
+
if role_match:
|
| 947 |
role_name = role_match.group(1).strip(" \"'")
|
| 948 |
member = ctx.guild.get_member(int(role_match.group(2)))
|
| 949 |
if not member:
|
|
|
|
| 953 |
return f"Role not found: {role_name}"
|
| 954 |
if not ctx.author.guild_permissions.manage_roles:
|
| 955 |
return "You need Manage Roles permission."
|
| 956 |
+
await member.add_roles(role, reason=f"AI Admin by {ctx.author}")
|
| 957 |
+
return f"OK: Added role **{role.name}** to {member.mention}"
|
| 958 |
+
|
| 959 |
+
remove_role_match = re.search(r"(?:remove)\s+role\s+(.+?)\s+(?:from)\s+<@!?(\d+)>", text, re.IGNORECASE)
|
| 960 |
+
if remove_role_match:
|
| 961 |
+
role_name = remove_role_match.group(1).strip(" \"'")
|
| 962 |
+
member = ctx.guild.get_member(int(remove_role_match.group(2)))
|
| 963 |
+
if not member:
|
| 964 |
+
return "Target member not found."
|
| 965 |
+
role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
|
| 966 |
+
if not role:
|
| 967 |
+
return f"Role not found: {role_name}"
|
| 968 |
+
if not ctx.author.guild_permissions.manage_roles:
|
| 969 |
+
return "You need Manage Roles permission."
|
| 970 |
+
await member.remove_roles(role, reason=f"AI Admin by {ctx.author}")
|
| 971 |
+
return f"OK: Removed role **{role.name}** from {member.mention}"
|
| 972 |
+
|
| 973 |
+
lock_match = re.search(r"^lock(?:\s+<#(\d+)>)?", lower)
|
| 974 |
+
if lock_match:
|
| 975 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 976 |
+
return "You need Manage Channels permission."
|
| 977 |
+
channel = ctx.guild.get_channel(int(lock_match.group(1))) if lock_match.group(1) else ctx.channel
|
| 978 |
+
if isinstance(channel, discord.TextChannel):
|
| 979 |
+
await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=f"AI Admin by {ctx.author}")
|
| 980 |
+
return f"OK: Locked {channel.mention}"
|
| 981 |
+
|
| 982 |
+
unlock_match = re.search(r"^unlock(?:\s+<#(\d+)>)?", lower)
|
| 983 |
+
if unlock_match:
|
| 984 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 985 |
+
return "You need Manage Channels permission."
|
| 986 |
+
channel = ctx.guild.get_channel(int(unlock_match.group(1))) if unlock_match.group(1) else ctx.channel
|
| 987 |
+
if isinstance(channel, discord.TextChannel):
|
| 988 |
+
await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=f"AI Admin by {ctx.author}")
|
| 989 |
+
return f"OK: Unlocked {channel.mention}"
|
| 990 |
+
|
| 991 |
+
rename_channel_match = re.search(
|
| 992 |
+
r"(?:rename)\s+channel\s+(.+?)\s+(?:to|->)\s+(.+)$",
|
| 993 |
+
text,
|
| 994 |
+
re.IGNORECASE,
|
| 995 |
+
)
|
| 996 |
+
if rename_channel_match:
|
| 997 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 998 |
+
return "You need Manage Channels permission."
|
| 999 |
+
old_ref = rename_channel_match.group(1).strip(" \"'")
|
| 1000 |
+
new_name = rename_channel_match.group(2).strip(" \"'")
|
| 1001 |
+
channel = ExecutionEngine._resolve_guild_channel(ctx.guild, old_ref)
|
| 1002 |
+
if not channel:
|
| 1003 |
+
return f"Channel not found: {old_ref}"
|
| 1004 |
+
if not new_name:
|
| 1005 |
+
return "New channel name is required."
|
| 1006 |
+
old_name = channel.name
|
| 1007 |
+
await channel.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
|
| 1008 |
+
return f"OK: Renamed channel **{old_name}** -> **{new_name[:100]}**"
|
| 1009 |
+
|
| 1010 |
+
delete_channel_match = re.search(r"(?:delete|remove)\s+channel\s+(.+)$", text, re.IGNORECASE)
|
| 1011 |
+
if delete_channel_match:
|
| 1012 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 1013 |
+
return "You need Manage Channels permission."
|
| 1014 |
+
ref = delete_channel_match.group(1).strip(" \"'")
|
| 1015 |
+
channel = ExecutionEngine._resolve_guild_channel(ctx.guild, ref)
|
| 1016 |
+
if not channel:
|
| 1017 |
+
return f"Channel not found: {ref}"
|
| 1018 |
+
if ctx.channel and channel.id == ctx.channel.id:
|
| 1019 |
+
return "Refusing to delete the channel currently being used for command execution."
|
| 1020 |
+
name = channel.name
|
| 1021 |
+
await channel.delete(reason=f"AI Admin by {ctx.author}")
|
| 1022 |
+
return f"OK: Deleted channel **{name}**"
|
| 1023 |
+
|
| 1024 |
+
rename_category_match = re.search(
|
| 1025 |
+
r"(?:rename)\s+(?:category|directory)\s+(.+?)\s+(?:to|->)\s+(.+)$",
|
| 1026 |
+
text,
|
| 1027 |
+
re.IGNORECASE,
|
| 1028 |
+
)
|
| 1029 |
+
if rename_category_match:
|
| 1030 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 1031 |
+
return "You need Manage Channels permission."
|
| 1032 |
+
old_ref = rename_category_match.group(1).strip(" \"'")
|
| 1033 |
+
new_name = rename_category_match.group(2).strip(" \"'")
|
| 1034 |
+
category = ExecutionEngine._resolve_category(ctx.guild, old_ref)
|
| 1035 |
+
if not category:
|
| 1036 |
+
return f"Category not found: {old_ref}"
|
| 1037 |
+
if not new_name:
|
| 1038 |
+
return "New category name is required."
|
| 1039 |
+
old_name = category.name
|
| 1040 |
+
await category.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
|
| 1041 |
+
return f"OK: Renamed category **{old_name}** -> **{new_name[:100]}**"
|
| 1042 |
+
|
| 1043 |
+
delete_category_match = re.search(r"(?:delete|remove)\s+(?:category|directory)\s+(.+)$", text, re.IGNORECASE)
|
| 1044 |
+
if delete_category_match:
|
| 1045 |
+
if not ctx.author.guild_permissions.manage_channels:
|
| 1046 |
+
return "You need Manage Channels permission."
|
| 1047 |
+
ref = delete_category_match.group(1).strip(" \"'")
|
| 1048 |
+
category = ExecutionEngine._resolve_category(ctx.guild, ref)
|
| 1049 |
+
if not category:
|
| 1050 |
+
return f"Category not found: {ref}"
|
| 1051 |
+
name = category.name
|
| 1052 |
+
channels_inside = len(category.channels)
|
| 1053 |
+
await category.delete(reason=f"AI Admin by {ctx.author}")
|
| 1054 |
+
return f"OK: Deleted category **{name}** (had {channels_inside} channel(s))"
|
| 1055 |
+
|
| 1056 |
+
return None
|
| 1057 |
+
|
| 1058 |
+
@commands.Cog.listener()
|
| 1059 |
+
async def on_ready(self) -> None:
|
| 1060 |
+
"""Execute any pending scheduled tasks on startup."""
|
| 1061 |
+
await self._check_scheduled_tasks()
|
| 1062 |
+
|
| 1063 |
+
async def _check_scheduled_tasks(self) -> None:
|
| 1064 |
+
"""Check and execute any due scheduled tasks."""
|
| 1065 |
+
now = dt.datetime.now(dt.timezone.utc).isoformat()
|
| 1066 |
+
tasks = await self.bot.db.fetchall(
|
| 1067 |
+
"SELECT id, guild_id, action_json, reason FROM ai_scheduled_tasks WHERE executed = 0 AND run_at <= ?",
|
| 1068 |
+
now,
|
| 1069 |
+
)
|
| 1070 |
+
for task_id, guild_id, action_json, reason in tasks:
|
| 1071 |
+
try:
|
| 1072 |
+
action = json.loads(action_json)
|
| 1073 |
+
guild = self.bot.get_guild(guild_id)
|
| 1074 |
+
if not guild:
|
| 1075 |
+
await self.bot.db.execute("UPDATE ai_scheduled_tasks SET executed = 1 WHERE id = ?", task_id)
|
| 1076 |
+
continue
|
| 1077 |
+
# Create a fake context for execution
|
| 1078 |
+
class _FakeCtx:
|
| 1079 |
+
pass
|
| 1080 |
+
fake_ctx = _FakeCtx()
|
| 1081 |
+
fake_ctx.guild = guild
|
| 1082 |
+
fake_ctx.author = guild.me
|
| 1083 |
+
fake_ctx.channel = guild.system_channel
|
| 1084 |
+
fake_ctx.send = lambda *a, **k: None # type: ignore
|
| 1085 |
+
await self.execution.execute([action], fake_ctx) # type: ignore
|
| 1086 |
+
await self.bot.db.execute("UPDATE ai_scheduled_tasks SET executed = 1 WHERE id = ?", task_id)
|
| 1087 |
+
except Exception:
|
| 1088 |
+
pass
|
| 1089 |
+
|
| 1090 |
+
@tasks.loop(minutes=5)
|
| 1091 |
+
async def _scheduled_task_checker(self) -> None:
|
| 1092 |
+
"""Periodically check for due tasks."""
|
| 1093 |
+
await self._check_scheduled_tasks()
|
| 1094 |
+
|
| 1095 |
async def cog_unload(self) -> None:
|
| 1096 |
+
self._scheduled_task_checker.cancel()
|
| 1097 |
await self.intelligence.close()
|
| 1098 |
|
| 1099 |
+
@commands.hybrid_command(name="ai_admin", description=get_cmd_desc("commands.ai.ai_admin_desc"))
|
| 1100 |
+
async def ai_admin(self, ctx: commands.Context, *, request: str) -> None:
|
| 1101 |
+
allowed, deny_reason = self.permission_guard.check(ctx)
|
| 1102 |
+
if not allowed:
|
| 1103 |
+
await ctx.send(deny_reason, ephemeral=True)
|
| 1104 |
+
return
|
| 1105 |
|
| 1106 |
direct_result = await self._try_direct_moderation(ctx, request)
|
| 1107 |
if direct_result is not None:
|
| 1108 |
await ctx.send(direct_result, ephemeral=True)
|
| 1109 |
return
|
| 1110 |
|
| 1111 |
+
if ctx.interaction and not ctx.interaction.response.is_done():
|
| 1112 |
+
try:
|
| 1113 |
+
await ctx.defer()
|
| 1114 |
+
except discord.InteractionResponded:
|
| 1115 |
+
pass
|
| 1116 |
|
| 1117 |
actions = await self.intelligence.ask_ai(request)
|
| 1118 |
if not actions:
|
|
|
|
| 1127 |
|
| 1128 |
results = await self.execution.execute(actions, ctx)
|
| 1129 |
|
| 1130 |
+
try:
|
| 1131 |
+
if response_text:
|
| 1132 |
+
await ctx.send(response_text)
|
| 1133 |
+
else:
|
| 1134 |
+
await ctx.send("\n".join(results))
|
| 1135 |
+
except discord.NotFound:
|
| 1136 |
+
# Interaction message may have been deleted
|
| 1137 |
+
try:
|
| 1138 |
+
if ctx.channel:
|
| 1139 |
+
await ctx.channel.send(response_text or "\n".join(results))
|
| 1140 |
+
except Exception:
|
| 1141 |
+
pass
|
| 1142 |
|
| 1143 |
+
@commands.hybrid_command(name="ai_help", description=get_cmd_desc("commands.ai.ai_help_desc"))
|
| 1144 |
async def ai_help(self, ctx: commands.Context) -> None:
|
| 1145 |
embed = discord.Embed(
|
| 1146 |
+
title="🛡️ AI Admin — Server Orchestrator",
|
| 1147 |
description=(
|
| 1148 |
+
"I'm your expert server manager. Ask me in Arabic or English and I'll handle it.\n\n"
|
| 1149 |
+
"**🏗️ Server Setup:**\n"
|
| 1150 |
+
"`/ai_admin جهز قسم البطولات` — Full section with category, channels, roles\n"
|
| 1151 |
+
"`/ai_admin create a staff room with private access` — Locked channel with permissions\n\n"
|
| 1152 |
+
"**📢 Announcements:**\n"
|
| 1153 |
+
"`/ai_admin announce the tournament starts at 9 PM` — Rich embed announcement\n\n"
|
| 1154 |
+
"**👥 Member Management:**\n"
|
| 1155 |
+
"`/ai_admin give VIP role to @user` — Role assignment\n"
|
| 1156 |
+
"`/ai_admin mute @user for 30m` — Timeout with reason\n\n"
|
| 1157 |
+
"**⏰ Scheduling:**\n"
|
| 1158 |
+
"`/ai_admin open events channel at 9 PM tonight` — Scheduled task\n"
|
| 1159 |
+
"`/ai_admin remove temp role after 1 week` — Future action\n\n"
|
| 1160 |
+
"**📊 Activity Analysis:**\n"
|
| 1161 |
+
"`/ai_admin who's most active in gaming?` — Member report\n\n"
|
| 1162 |
+
"**🎨 Direct Moderation Shortcuts:**\n"
|
| 1163 |
+
"`/ai_admin kick @user` | `ban @user` | `mute @user 30m`\n"
|
| 1164 |
+
"`/ai_admin lock #channel` | `unlock #channel`\n"
|
| 1165 |
+
"`/ai_admin purge 50 from #general`"
|
| 1166 |
+
),
|
| 1167 |
+
color=discord.Color.blue()
|
| 1168 |
+
)
|
| 1169 |
+
embed.set_footer(text="AI Admin v2.0 — Server Orchestrator")
|
| 1170 |
+
try:
|
| 1171 |
+
await ctx.send(embed=embed, ephemeral=True)
|
| 1172 |
+
except discord.NotFound:
|
| 1173 |
+
if ctx.channel:
|
| 1174 |
+
await ctx.channel.send(embed=embed)
|
|
|
|
|
|
|
|
|
|
| 1175 |
|
| 1176 |
|
| 1177 |
async def setup(bot: commands.Bot) -> None:
|
bot/cogs/ai_suite.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bot/cogs/board_games.py
CHANGED
|
@@ -1,824 +1,825 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import random
|
| 4 |
-
from dataclasses import dataclass
|
| 5 |
-
|
| 6 |
-
import discord
|
| 7 |
-
from discord.ext import commands
|
| 8 |
-
from bot.
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
self.
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
self.bot.add_view(
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
list("
|
| 280 |
-
list("
|
| 281 |
-
list("▫▫▫▫▫▫▫▫"),
|
| 282 |
-
list("▫▫▫▫▫▫▫▫"),
|
| 283 |
-
list("▫▫▫▫▫▫▫▫"),
|
| 284 |
-
list("
|
| 285 |
-
list("
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
for
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
b
|
| 306 |
-
b[3][
|
| 307 |
-
b[
|
| 308 |
-
b[4][
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
ny
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
winner_id,
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
await self.
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
s.board[
|
| 641 |
-
s.
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
embed
|
| 650 |
-
|
| 651 |
-
"•
|
| 652 |
-
"•
|
| 653 |
-
"•
|
| 654 |
-
"
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
await self.
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
if
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
s.board[
|
| 748 |
-
s.board[
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
await self.
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
winner_id,
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
|
| 6 |
+
import discord
|
| 7 |
+
from discord.ext import commands
|
| 8 |
+
from bot.i18n import get_cmd_desc
|
| 9 |
+
from bot.emojis import ui
|
| 10 |
+
|
| 11 |
+
FILES = "abcdefgh"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class BoardGameSession:
|
| 16 |
+
game: str
|
| 17 |
+
players: tuple[int, int] # second player can be 0 for bot
|
| 18 |
+
turn: int
|
| 19 |
+
board: list[list[str]]
|
| 20 |
+
tournament_name: str | None = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class BoardMoveModal(discord.ui.Modal, title="Play Move"):
|
| 26 |
+
move = discord.ui.TextInput(label="Move", placeholder="e2e4 / d3 / 4", max_length=10)
|
| 27 |
+
|
| 28 |
+
def __init__(self, cog: "BoardGames") -> None:
|
| 29 |
+
super().__init__(timeout=None)
|
| 30 |
+
self.cog = cog
|
| 31 |
+
|
| 32 |
+
async def on_submit(self, interaction: discord.Interaction) -> None:
|
| 33 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 34 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 35 |
+
return
|
| 36 |
+
err = await self.cog._play_turn(
|
| 37 |
+
guild=interaction.guild,
|
| 38 |
+
channel=interaction.channel,
|
| 39 |
+
author_id=interaction.user.id,
|
| 40 |
+
move=str(self.move.value).strip(),
|
| 41 |
+
)
|
| 42 |
+
if err:
|
| 43 |
+
await interaction.response.send_message(err, ephemeral=True)
|
| 44 |
+
else:
|
| 45 |
+
await interaction.response.send_message("✅ Move accepted.", ephemeral=True)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class BoardActionView(discord.ui.View):
|
| 49 |
+
def __init__(self, cog: "BoardGames") -> None:
|
| 50 |
+
super().__init__(timeout=None)
|
| 51 |
+
self.cog = cog
|
| 52 |
+
|
| 53 |
+
@discord.ui.button(label="Play Move", style=discord.ButtonStyle.primary, emoji=ui("controller"), custom_id="board_play")
|
| 54 |
+
async def play_move(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 55 |
+
await interaction.response.send_modal(BoardMoveModal(self.cog))
|
| 56 |
+
|
| 57 |
+
@discord.ui.button(label="Forfeit", style=discord.ButtonStyle.danger, emoji=ui("no"), custom_id="board_forfeit")
|
| 58 |
+
async def forfeit(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 59 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 60 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 61 |
+
return
|
| 62 |
+
err = await self.cog._forfeit(
|
| 63 |
+
guild=interaction.guild,
|
| 64 |
+
channel=interaction.channel,
|
| 65 |
+
author_id=interaction.user.id,
|
| 66 |
+
)
|
| 67 |
+
if err:
|
| 68 |
+
await interaction.response.send_message(err, ephemeral=True)
|
| 69 |
+
return
|
| 70 |
+
await interaction.response.send_message("🏳️ Forfeit processed.", ephemeral=True)
|
| 71 |
+
|
| 72 |
+
@discord.ui.button(label="End Game", style=discord.ButtonStyle.danger, emoji=ui("lock"), custom_id="board_end")
|
| 73 |
+
async def end_game(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 74 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 75 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 76 |
+
return
|
| 77 |
+
session = self.cog.sessions.get(interaction.channel.id)
|
| 78 |
+
if not session:
|
| 79 |
+
await interaction.response.send_message("No active board game in this channel.", ephemeral=True)
|
| 80 |
+
return
|
| 81 |
+
if interaction.user.id not in session.players and not interaction.user.guild_permissions.manage_channels:
|
| 82 |
+
await interaction.response.send_message("Only players/admins can end this game.", ephemeral=True)
|
| 83 |
+
return
|
| 84 |
+
self.cog.sessions.pop(interaction.channel.id, None)
|
| 85 |
+
await interaction.response.send_message("🔒 Game session ended.", ephemeral=True)
|
| 86 |
+
if interaction.channel.name.startswith("game-"):
|
| 87 |
+
try:
|
| 88 |
+
await interaction.channel.delete(reason=f"Game room closed by {interaction.user}")
|
| 89 |
+
except Exception:
|
| 90 |
+
pass
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class Connect4ActionView(BoardActionView):
|
| 94 |
+
def __init__(self, cog: "BoardGames") -> None:
|
| 95 |
+
super().__init__(cog)
|
| 96 |
+
self.clear_items()
|
| 97 |
+
for col in range(1, 8):
|
| 98 |
+
button = discord.ui.Button(label=str(col), style=discord.ButtonStyle.secondary, row=0 if col <= 4 else 1)
|
| 99 |
+
|
| 100 |
+
async def _drop(interaction: discord.Interaction, c: int = col) -> None:
|
| 101 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 102 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 103 |
+
return
|
| 104 |
+
err = await self.cog._play_turn(
|
| 105 |
+
guild=interaction.guild,
|
| 106 |
+
channel=interaction.channel,
|
| 107 |
+
author_id=interaction.user.id,
|
| 108 |
+
move=str(c),
|
| 109 |
+
)
|
| 110 |
+
if err:
|
| 111 |
+
await interaction.response.send_message(err, ephemeral=True)
|
| 112 |
+
else:
|
| 113 |
+
await interaction.response.send_message(f"✅ Dropped in column {c}", ephemeral=True)
|
| 114 |
+
|
| 115 |
+
button.callback = _drop
|
| 116 |
+
self.add_item(button)
|
| 117 |
+
|
| 118 |
+
forfeit_btn = discord.ui.Button(label="Forfeit", style=discord.ButtonStyle.danger, emoji=ui("no"), row=2, custom_id="board_forfeit_c4")
|
| 119 |
+
end_btn = discord.ui.Button(label="End Game", style=discord.ButtonStyle.danger, emoji=ui("lock"), row=2, custom_id="board_end_c4")
|
| 120 |
+
|
| 121 |
+
async def _forfeit(interaction: discord.Interaction) -> None:
|
| 122 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 123 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 124 |
+
return
|
| 125 |
+
err = await self.cog._forfeit(guild=interaction.guild, channel=interaction.channel, author_id=interaction.user.id)
|
| 126 |
+
if err:
|
| 127 |
+
await interaction.response.send_message(err, ephemeral=True)
|
| 128 |
+
return
|
| 129 |
+
await interaction.response.send_message("🏳️ Forfeit processed.", ephemeral=True)
|
| 130 |
+
|
| 131 |
+
forfeit_btn.callback = _forfeit
|
| 132 |
+
self.add_item(forfeit_btn)
|
| 133 |
+
|
| 134 |
+
async def _end(interaction: discord.Interaction) -> None:
|
| 135 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 136 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 137 |
+
return
|
| 138 |
+
session = self.cog.sessions.get(interaction.channel.id)
|
| 139 |
+
if not session:
|
| 140 |
+
await interaction.response.send_message("No active board game in this channel.", ephemeral=True)
|
| 141 |
+
return
|
| 142 |
+
if interaction.user.id not in session.players and not interaction.user.guild_permissions.manage_channels:
|
| 143 |
+
await interaction.response.send_message("Only players/admins can end this game.", ephemeral=True)
|
| 144 |
+
return
|
| 145 |
+
self.cog.sessions.pop(interaction.channel.id, None)
|
| 146 |
+
await interaction.response.send_message("🔒 Game session ended.", ephemeral=True)
|
| 147 |
+
if interaction.channel.name.startswith("game-"):
|
| 148 |
+
try:
|
| 149 |
+
await interaction.channel.delete(reason=f"Game room closed by {interaction.user}")
|
| 150 |
+
except Exception:
|
| 151 |
+
pass
|
| 152 |
+
|
| 153 |
+
end_btn.callback = _end
|
| 154 |
+
self.add_item(end_btn)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
import random as _random
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class QuickRPSView(discord.ui.View):
|
| 161 |
+
def __init__(self) -> None:
|
| 162 |
+
super().__init__(timeout=60)
|
| 163 |
+
|
| 164 |
+
@discord.ui.button(label="Rock 🪨", style=discord.ButtonStyle.primary)
|
| 165 |
+
async def rock(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 166 |
+
await self._resolve(interaction, "rock")
|
| 167 |
+
|
| 168 |
+
@discord.ui.button(label="Paper 📄", style=discord.ButtonStyle.primary)
|
| 169 |
+
async def paper(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 170 |
+
await self._resolve(interaction, "paper")
|
| 171 |
+
|
| 172 |
+
@discord.ui.button(label="Scissors ✂️", style=discord.ButtonStyle.primary)
|
| 173 |
+
async def scissors(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 174 |
+
await self._resolve(interaction, "scissors")
|
| 175 |
+
|
| 176 |
+
async def _resolve(self, interaction: discord.Interaction, choice: str) -> None:
|
| 177 |
+
bot_choice = _random.choice(["rock", "paper", "scissors"])
|
| 178 |
+
emoji_map = {"rock": "🪨", "paper": "📄", "scissors": "✂️"}
|
| 179 |
+
result_map = {("rock", "scissors"): "win", ("paper", "rock"): "win", ("scissors", "paper"): "win"}
|
| 180 |
+
if choice == bot_choice:
|
| 181 |
+
result = "🤝 Tie!"
|
| 182 |
+
elif (choice, bot_choice) in result_map:
|
| 183 |
+
result = "🎉 You win!"
|
| 184 |
+
else:
|
| 185 |
+
result = "💀 You lose!"
|
| 186 |
+
await interaction.response.send_message(f"You: {emoji_map[choice]} | Bot: {emoji_map[bot_choice]}\n{result}", ephemeral=True)
|
| 187 |
+
self.stop()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class GamePanelView(discord.ui.View):
|
| 191 |
+
def __init__(self, cog: "BoardGames") -> None:
|
| 192 |
+
super().__init__(timeout=None)
|
| 193 |
+
self.cog = cog
|
| 194 |
+
|
| 195 |
+
async def _start(self, interaction: discord.Interaction, game: str) -> None:
|
| 196 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 197 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 198 |
+
return
|
| 199 |
+
ok = await self.cog.start_game_session_interaction(interaction, game)
|
| 200 |
+
if ok:
|
| 201 |
+
await interaction.response.send_message(f"✅ Started **{game}** vs bot.", ephemeral=True)
|
| 202 |
+
|
| 203 |
+
@discord.ui.button(label="Chess", style=discord.ButtonStyle.primary, emoji=ui("chess"), row=0, custom_id="board_chess")
|
| 204 |
+
async def chess(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 205 |
+
await self._start(interaction, "chess")
|
| 206 |
+
|
| 207 |
+
@discord.ui.button(label="Checkers", style=discord.ButtonStyle.secondary, emoji=ui("joystick"), row=0, custom_id="board_checkers")
|
| 208 |
+
async def checkers(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 209 |
+
await self._start(interaction, "checkers")
|
| 210 |
+
|
| 211 |
+
@discord.ui.button(label="Connect4", style=discord.ButtonStyle.success, emoji=ui("joystick"), row=0, custom_id="board_connect4")
|
| 212 |
+
async def connect4(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 213 |
+
await self._start(interaction, "connect4")
|
| 214 |
+
|
| 215 |
+
@discord.ui.button(label="Othello", style=discord.ButtonStyle.blurple, emoji=ui("joystick"), row=0, custom_id="board_othello")
|
| 216 |
+
async def othello(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 217 |
+
await self._start(interaction, "othello")
|
| 218 |
+
|
| 219 |
+
@discord.ui.button(label="TicTacToe", style=discord.ButtonStyle.primary, emoji=ui("x"), row=1, custom_id="board_tictactoe")
|
| 220 |
+
async def tictactoe(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 221 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 222 |
+
await interaction.response.send_message("Server text channels only.", ephemeral=True)
|
| 223 |
+
return
|
| 224 |
+
await interaction.response.send_message(f"❎ **TicTacToe**: {interaction.user.mention} vs 🤖 Bot\nUse `/xo` to play!", ephemeral=True)
|
| 225 |
+
|
| 226 |
+
@discord.ui.button(label="Rock Paper Scissors", style=discord.ButtonStyle.secondary, emoji="✂️", row=1, custom_id="board_rps")
|
| 227 |
+
async def rps(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 228 |
+
if not interaction.guild:
|
| 229 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 230 |
+
return
|
| 231 |
+
await interaction.response.send_message(
|
| 232 |
+
"Choose your move:",
|
| 233 |
+
view=QuickRPSView(),
|
| 234 |
+
ephemeral=True,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
class BoardGames(commands.Cog):
|
| 239 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 240 |
+
self.bot = bot
|
| 241 |
+
self.sessions: dict[int, BoardGameSession] = {}
|
| 242 |
+
|
| 243 |
+
async def cog_load(self) -> None:
|
| 244 |
+
self.bot.add_view(GamePanelView(self))
|
| 245 |
+
self.bot.add_view(BoardActionView(self))
|
| 246 |
+
|
| 247 |
+
def _coord(self, token: str) -> tuple[int, int] | None:
|
| 248 |
+
token = token.strip().lower()
|
| 249 |
+
if len(token) != 2 or token[0] not in FILES or token[1] not in "12345678":
|
| 250 |
+
return None
|
| 251 |
+
x = FILES.index(token[0])
|
| 252 |
+
y = 8 - int(token[1])
|
| 253 |
+
return y, x
|
| 254 |
+
|
| 255 |
+
def _coord_to_text(self, y: int, x: int) -> str:
|
| 256 |
+
return f"{FILES[x]}{8-y}"
|
| 257 |
+
|
| 258 |
+
def _current_player(self, s: BoardGameSession) -> int:
|
| 259 |
+
return s.players[s.turn]
|
| 260 |
+
|
| 261 |
+
def _render(self, s: BoardGameSession) -> str:
|
| 262 |
+
if s.game == "connect4":
|
| 263 |
+
lines = ["1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣"]
|
| 264 |
+
for row in s.board:
|
| 265 |
+
lines.append("".join(row))
|
| 266 |
+
return "\n".join(lines)
|
| 267 |
+
if s.game == "othello":
|
| 268 |
+
lines = ["⬛🇦🇧🇨🇩🇪🇫🇬🇭"]
|
| 269 |
+
for y, row in enumerate(s.board):
|
| 270 |
+
lines.append(f"{8-y}️⃣" + "".join(row))
|
| 271 |
+
return "\n".join(lines)
|
| 272 |
+
lines = [" 🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭"]
|
| 273 |
+
for y, row in enumerate(s.board):
|
| 274 |
+
lines.append(f"{8-y} " + " ".join(row))
|
| 275 |
+
return "\n".join(lines)
|
| 276 |
+
|
| 277 |
+
def _make_chess(self) -> list[list[str]]:
|
| 278 |
+
return [
|
| 279 |
+
list("♜♞♝♛♚♝♞♜"),
|
| 280 |
+
list("♟♟♟♟♟♟♟♟"),
|
| 281 |
+
list("▫▫▫▫▫▫▫▫"),
|
| 282 |
+
list("▫▫▫▫▫▫▫▫"),
|
| 283 |
+
list("▫▫▫▫▫▫▫▫"),
|
| 284 |
+
list("▫▫▫▫▫▫▫▫"),
|
| 285 |
+
list("♙♙♙♙♙♙♙♙"),
|
| 286 |
+
list("♖♘♗♕♔♗♘♖"),
|
| 287 |
+
]
|
| 288 |
+
|
| 289 |
+
def _make_checkers(self) -> list[list[str]]:
|
| 290 |
+
board = [["▫" for _ in range(8)] for _ in range(8)]
|
| 291 |
+
for y in range(3):
|
| 292 |
+
for x in range(8):
|
| 293 |
+
if (x + y) % 2 == 1:
|
| 294 |
+
board[y][x] = "🔴"
|
| 295 |
+
for y in range(5, 8):
|
| 296 |
+
for x in range(8):
|
| 297 |
+
if (x + y) % 2 == 1:
|
| 298 |
+
board[y][x] = "⚪"
|
| 299 |
+
return board
|
| 300 |
+
|
| 301 |
+
def _make_connect4(self) -> list[list[str]]:
|
| 302 |
+
return [["⚫" for _ in range(7)] for _ in range(6)]
|
| 303 |
+
|
| 304 |
+
def _make_othello(self) -> list[list[str]]:
|
| 305 |
+
b = [["🟩" for _ in range(8)] for _ in range(8)]
|
| 306 |
+
b[3][3] = "⚪"
|
| 307 |
+
b[3][4] = "⚫"
|
| 308 |
+
b[4][3] = "⚫"
|
| 309 |
+
b[4][4] = "⚪"
|
| 310 |
+
return b
|
| 311 |
+
|
| 312 |
+
async def _announce(self, ctx: commands.Context, s: BoardGameSession, text: str) -> None:
|
| 313 |
+
p1 = f"<@{s.players[0]}>"
|
| 314 |
+
p2 = "🤖 Bot" if s.players[1] == 0 else f"<@{s.players[1]}>"
|
| 315 |
+
turn = self._current_player(s)
|
| 316 |
+
tname = "🤖 Bot" if turn == 0 else f"<@{turn}>"
|
| 317 |
+
await ctx.send(
|
| 318 |
+
f"**{s.game.title()}** | {p1} vs {p2}\n{text}\nTurn: {tname}\n{self._render(s)}"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
async def start_game_session(
|
| 322 |
+
self,
|
| 323 |
+
ctx: commands.Context,
|
| 324 |
+
game: str,
|
| 325 |
+
opponent: discord.Member | None = None,
|
| 326 |
+
tournament_name: str | None = None,
|
| 327 |
+
) -> bool:
|
| 328 |
+
game = game.lower().strip()
|
| 329 |
+
if game not in {"chess", "checkers", "connect4", "othello"}:
|
| 330 |
+
await ctx.reply("Supported: chess, checkers, connect4, othello")
|
| 331 |
+
return False
|
| 332 |
+
if ctx.channel.id in self.sessions:
|
| 333 |
+
await ctx.reply("There is already an active board game in this channel.")
|
| 334 |
+
return False
|
| 335 |
+
|
| 336 |
+
opponent_id = opponent.id if opponent else 0
|
| 337 |
+
if opponent_id == ctx.author.id:
|
| 338 |
+
await ctx.reply("Choose another player or leave opponent empty for bot.")
|
| 339 |
+
return False
|
| 340 |
+
|
| 341 |
+
if game == "chess":
|
| 342 |
+
board = self._make_chess()
|
| 343 |
+
elif game == "checkers":
|
| 344 |
+
board = self._make_checkers()
|
| 345 |
+
elif game == "connect4":
|
| 346 |
+
board = self._make_connect4()
|
| 347 |
+
else:
|
| 348 |
+
board = self._make_othello()
|
| 349 |
+
|
| 350 |
+
s = BoardGameSession(game=game, players=(ctx.author.id, opponent_id), turn=0, board=board, tournament_name=tournament_name)
|
| 351 |
+
self.sessions[ctx.channel.id] = s
|
| 352 |
+
await self._announce(ctx.channel, s, "Game started. Use buttons or `/board_move`.")
|
| 353 |
+
return True
|
| 354 |
+
|
| 355 |
+
async def start_game_session_interaction(self, interaction: discord.Interaction, game: str) -> bool:
|
| 356 |
+
if not interaction.guild or not isinstance(interaction.channel, discord.TextChannel):
|
| 357 |
+
return False
|
| 358 |
+
game = game.lower().strip()
|
| 359 |
+
if game not in {"chess", "checkers", "connect4", "othello"}:
|
| 360 |
+
await interaction.followup.send("Supported: chess, checkers, connect4, othello", ephemeral=True)
|
| 361 |
+
return False
|
| 362 |
+
if interaction.channel.id in self.sessions:
|
| 363 |
+
await interaction.followup.send("There is already an active board game in this channel.", ephemeral=True)
|
| 364 |
+
return False
|
| 365 |
+
|
| 366 |
+
if game == "chess":
|
| 367 |
+
board = self._make_chess()
|
| 368 |
+
elif game == "checkers":
|
| 369 |
+
board = self._make_checkers()
|
| 370 |
+
elif game == "connect4":
|
| 371 |
+
board = self._make_connect4()
|
| 372 |
+
else:
|
| 373 |
+
board = self._make_othello()
|
| 374 |
+
|
| 375 |
+
s = BoardGameSession(game=game, players=(interaction.user.id, 0), turn=0, board=board)
|
| 376 |
+
self.sessions[interaction.channel.id] = s
|
| 377 |
+
await self._announce(interaction.channel, s, "Game started from panel.")
|
| 378 |
+
return True
|
| 379 |
+
|
| 380 |
+
async def start_tournament_duel(
|
| 381 |
+
self,
|
| 382 |
+
ctx: commands.Context,
|
| 383 |
+
game: str,
|
| 384 |
+
player1: discord.Member,
|
| 385 |
+
player2: discord.Member | None,
|
| 386 |
+
tournament_name: str,
|
| 387 |
+
) -> bool:
|
| 388 |
+
if ctx.channel.id in self.sessions:
|
| 389 |
+
await ctx.reply("There is already an active board game in this channel.")
|
| 390 |
+
return False
|
| 391 |
+
game = game.lower().strip()
|
| 392 |
+
if game == "chess":
|
| 393 |
+
board = self._make_chess()
|
| 394 |
+
elif game == "checkers":
|
| 395 |
+
board = self._make_checkers()
|
| 396 |
+
elif game == "connect4":
|
| 397 |
+
board = self._make_connect4()
|
| 398 |
+
elif game == "othello":
|
| 399 |
+
board = self._make_othello()
|
| 400 |
+
else:
|
| 401 |
+
await ctx.reply("Supported: chess, checkers, connect4, othello")
|
| 402 |
+
return False
|
| 403 |
+
|
| 404 |
+
s = BoardGameSession(
|
| 405 |
+
game=game,
|
| 406 |
+
players=(player1.id, player2.id if player2 else 0),
|
| 407 |
+
turn=0,
|
| 408 |
+
board=board,
|
| 409 |
+
tournament_name=tournament_name,
|
| 410 |
+
)
|
| 411 |
+
self.sessions[ctx.channel.id] = s
|
| 412 |
+
await self._announce(ctx.channel, s, f"Tournament duel: **{tournament_name}**")
|
| 413 |
+
return True
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _in_bounds(self, y: int, x: int) -> bool:
|
| 417 |
+
return 0 <= y < 8 and 0 <= x < 8
|
| 418 |
+
|
| 419 |
+
def _is_white(self, p: str) -> bool:
|
| 420 |
+
return p in "♙♖♘♗♕♔"
|
| 421 |
+
|
| 422 |
+
def _is_black(self, p: str) -> bool:
|
| 423 |
+
return p in "♟♜♞♝♛♚"
|
| 424 |
+
|
| 425 |
+
def _enemy(self, p: str, white_turn: bool) -> bool:
|
| 426 |
+
return self._is_black(p) if white_turn else self._is_white(p)
|
| 427 |
+
|
| 428 |
+
def _friend(self, p: str, white_turn: bool) -> bool:
|
| 429 |
+
return self._is_white(p) if white_turn else self._is_black(p)
|
| 430 |
+
|
| 431 |
+
def _chess_legal(self, b: list[list[str]], src: tuple[int, int], dst: tuple[int, int], white_turn: bool) -> bool:
|
| 432 |
+
sy, sx = src
|
| 433 |
+
dy, dx = dst
|
| 434 |
+
if not (self._in_bounds(sy, sx) and self._in_bounds(dy, dx)):
|
| 435 |
+
return False
|
| 436 |
+
piece = b[sy][sx]
|
| 437 |
+
if piece == "▫" or not self._friend(piece, white_turn):
|
| 438 |
+
return False
|
| 439 |
+
target = b[dy][dx]
|
| 440 |
+
if target != "▫" and self._friend(target, white_turn):
|
| 441 |
+
return False
|
| 442 |
+
|
| 443 |
+
vy = dy - sy
|
| 444 |
+
vx = dx - sx
|
| 445 |
+
ady, adx = abs(vy), abs(vx)
|
| 446 |
+
|
| 447 |
+
def clear_line(stepy: int, stepx: int) -> bool:
|
| 448 |
+
y, x = sy + stepy, sx + stepx
|
| 449 |
+
while (y, x) != (dy, dx):
|
| 450 |
+
if b[y][x] != "▫":
|
| 451 |
+
return False
|
| 452 |
+
y += stepy
|
| 453 |
+
x += stepx
|
| 454 |
+
return True
|
| 455 |
+
|
| 456 |
+
if piece in "♙♟":
|
| 457 |
+
direction = -1 if piece == "♙" else 1
|
| 458 |
+
start_row = 6 if piece == "♙" else 1
|
| 459 |
+
if vx == 0 and target == "▫":
|
| 460 |
+
if vy == direction:
|
| 461 |
+
return True
|
| 462 |
+
if sy == start_row and vy == 2 * direction and b[sy + direction][sx] == "▫":
|
| 463 |
+
return True
|
| 464 |
+
if adx == 1 and vy == direction and target != "▫" and self._enemy(target, white_turn):
|
| 465 |
+
return True
|
| 466 |
+
return False
|
| 467 |
+
if piece in "♖♜":
|
| 468 |
+
if sx == dx and clear_line(1 if vy > 0 else -1, 0):
|
| 469 |
+
return True
|
| 470 |
+
if sy == dy and clear_line(0, 1 if vx > 0 else -1):
|
| 471 |
+
return True
|
| 472 |
+
return False
|
| 473 |
+
if piece in "♗♝":
|
| 474 |
+
if adx == ady and clear_line(1 if vy > 0 else -1, 1 if vx > 0 else -1):
|
| 475 |
+
return True
|
| 476 |
+
return False
|
| 477 |
+
if piece in "♕♛":
|
| 478 |
+
if sx == dx and clear_line(1 if vy > 0 else -1, 0):
|
| 479 |
+
return True
|
| 480 |
+
if sy == dy and clear_line(0, 1 if vx > 0 else -1):
|
| 481 |
+
return True
|
| 482 |
+
if adx == ady and clear_line(1 if vy > 0 else -1, 1 if vx > 0 else -1):
|
| 483 |
+
return True
|
| 484 |
+
return False
|
| 485 |
+
if piece in "♘♞":
|
| 486 |
+
return (ady, adx) in {(1, 2), (2, 1)}
|
| 487 |
+
if piece in "♔♚":
|
| 488 |
+
return max(ady, adx) == 1
|
| 489 |
+
return False
|
| 490 |
+
|
| 491 |
+
def _checkers_legal(self, b: list[list[str]], src: tuple[int, int], dst: tuple[int, int], white_turn: bool) -> bool:
|
| 492 |
+
sy, sx = src
|
| 493 |
+
dy, dx = dst
|
| 494 |
+
if not (self._in_bounds(sy, sx) and self._in_bounds(dy, dx)):
|
| 495 |
+
return False
|
| 496 |
+
me = "⚪" if white_turn else "🔴"
|
| 497 |
+
enemy = "🔴" if white_turn else "⚪"
|
| 498 |
+
if b[sy][sx] != me or b[dy][dx] != "▫":
|
| 499 |
+
return False
|
| 500 |
+
vy, vx = dy - sy, dx - sx
|
| 501 |
+
direction = -1 if white_turn else 1
|
| 502 |
+
if abs(vx) == 1 and vy == direction:
|
| 503 |
+
return True
|
| 504 |
+
if abs(vx) == 2 and vy == 2 * direction:
|
| 505 |
+
my, mx = sy + direction, sx + (1 if vx > 0 else -1)
|
| 506 |
+
return b[my][mx] == enemy
|
| 507 |
+
return False
|
| 508 |
+
|
| 509 |
+
def _apply_checkers_capture(self, b: list[list[str]], src: tuple[int, int], dst: tuple[int, int], white_turn: bool) -> None:
|
| 510 |
+
sy, sx = src
|
| 511 |
+
dy, dx = dst
|
| 512 |
+
if abs(dy - sy) == 2:
|
| 513 |
+
my, mx = (sy + dy) // 2, (sx + dx) // 2
|
| 514 |
+
b[my][mx] = "▫"
|
| 515 |
+
|
| 516 |
+
def _connect4_drop(self, b: list[list[str]], col: int, white_turn: bool) -> tuple[int, int] | None:
|
| 517 |
+
token = "🔵" if white_turn else "🟠"
|
| 518 |
+
for y in range(len(b) - 1, -1, -1):
|
| 519 |
+
if b[y][col] == "⚫":
|
| 520 |
+
b[y][col] = token
|
| 521 |
+
return y, col
|
| 522 |
+
return None
|
| 523 |
+
|
| 524 |
+
def _has_four(self, b: list[list[str]], y: int, x: int) -> bool:
|
| 525 |
+
token = b[y][x]
|
| 526 |
+
if token == "⚫":
|
| 527 |
+
return False
|
| 528 |
+
dirs = [(1, 0), (0, 1), (1, 1), (1, -1)]
|
| 529 |
+
h, w = len(b), len(b[0])
|
| 530 |
+
for dy, dx in dirs:
|
| 531 |
+
c = 1
|
| 532 |
+
for sign in (1, -1):
|
| 533 |
+
ny, nx = y, x
|
| 534 |
+
while True:
|
| 535 |
+
ny += dy * sign
|
| 536 |
+
nx += dx * sign
|
| 537 |
+
if not (0 <= ny < h and 0 <= nx < w) or b[ny][nx] != token:
|
| 538 |
+
break
|
| 539 |
+
c += 1
|
| 540 |
+
if c >= 4:
|
| 541 |
+
return True
|
| 542 |
+
return False
|
| 543 |
+
|
| 544 |
+
def _othello_flips(self, b: list[list[str]], y: int, x: int, black_turn: bool) -> list[tuple[int, int]]:
|
| 545 |
+
me = "⚫" if black_turn else "⚪"
|
| 546 |
+
enemy = "⚪" if black_turn else "⚫"
|
| 547 |
+
if not self._in_bounds(y, x) or b[y][x] != "🟩":
|
| 548 |
+
return []
|
| 549 |
+
flips: list[tuple[int, int]] = []
|
| 550 |
+
for dy in (-1, 0, 1):
|
| 551 |
+
for dx in (-1, 0, 1):
|
| 552 |
+
if dy == 0 and dx == 0:
|
| 553 |
+
continue
|
| 554 |
+
path: list[tuple[int, int]] = []
|
| 555 |
+
ny, nx = y + dy, x + dx
|
| 556 |
+
while self._in_bounds(ny, nx) and b[ny][nx] == enemy:
|
| 557 |
+
path.append((ny, nx))
|
| 558 |
+
ny += dy
|
| 559 |
+
nx += dx
|
| 560 |
+
if path and self._in_bounds(ny, nx) and b[ny][nx] == me:
|
| 561 |
+
flips.extend(path)
|
| 562 |
+
return flips
|
| 563 |
+
|
| 564 |
+
async def _finish(self, ctx: commands.Context, winner_id: int | None, reason: str) -> None:
|
| 565 |
+
s = self.sessions.pop(ctx.channel.id, None)
|
| 566 |
+
if not s:
|
| 567 |
+
return
|
| 568 |
+
if winner_id is None:
|
| 569 |
+
await ctx.send(f"🤝 Draw | {reason}")
|
| 570 |
+
return
|
| 571 |
+
|
| 572 |
+
if winner_id == 0:
|
| 573 |
+
await ctx.send(f"🤖 Bot won | {reason}")
|
| 574 |
+
return
|
| 575 |
+
|
| 576 |
+
await ctx.send(f"🏆 Winner: <@{winner_id}> | {reason}")
|
| 577 |
+
if s.tournament_name:
|
| 578 |
+
await self.bot.db.execute(
|
| 579 |
+
"UPDATE tournaments SET winner_id = ?, status = 'finished' WHERE guild_id = ? AND name = ?",
|
| 580 |
+
winner_id,
|
| 581 |
+
ctx.guild.id,
|
| 582 |
+
s.tournament_name,
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
async def _bot_move(self, ctx: commands.Context) -> None:
|
| 586 |
+
s = self.sessions.get(ctx.channel.id)
|
| 587 |
+
if not s or s.players[1] != 0 or self._current_player(s) != 0:
|
| 588 |
+
return
|
| 589 |
+
if s.game == "connect4":
|
| 590 |
+
valid = [c for c in range(7) if s.board[0][c] == "⚫"]
|
| 591 |
+
col = random.choice(valid)
|
| 592 |
+
spot = self._connect4_drop(s.board, col, white_turn=False)
|
| 593 |
+
if spot and self._has_four(s.board, spot[0], spot[1]):
|
| 594 |
+
await self._announce(ctx.channel, s, f"Bot played column {col + 1}.")
|
| 595 |
+
await self._finish(ctx, 0, "Four in a row!")
|
| 596 |
+
return
|
| 597 |
+
s.turn = 0
|
| 598 |
+
await self._announce(ctx.channel, s, f"Bot played column {col + 1}.")
|
| 599 |
+
return
|
| 600 |
+
|
| 601 |
+
if s.game == "othello":
|
| 602 |
+
legal: list[tuple[int, int, list[tuple[int, int]]]] = []
|
| 603 |
+
for y in range(8):
|
| 604 |
+
for x in range(8):
|
| 605 |
+
flips = self._othello_flips(s.board, y, x, black_turn=False)
|
| 606 |
+
if flips:
|
| 607 |
+
legal.append((y, x, flips))
|
| 608 |
+
if not legal:
|
| 609 |
+
s.turn = 0
|
| 610 |
+
await self._announce(ctx.channel, s, "Bot has no valid move and passes.")
|
| 611 |
+
return
|
| 612 |
+
y, x, flips = random.choice(legal)
|
| 613 |
+
s.board[y][x] = "⚪"
|
| 614 |
+
for fy, fx in flips:
|
| 615 |
+
s.board[fy][fx] = "⚪"
|
| 616 |
+
s.turn = 0
|
| 617 |
+
await self._announce(ctx.channel, s, f"Bot played {self._coord_to_text(y, x)}")
|
| 618 |
+
return
|
| 619 |
+
|
| 620 |
+
# chess/checkers bot move: random pseudo legal
|
| 621 |
+
moves: list[tuple[tuple[int, int], tuple[int, int]]] = []
|
| 622 |
+
for sy in range(8):
|
| 623 |
+
for sx in range(8):
|
| 624 |
+
for dy in range(8):
|
| 625 |
+
for dx in range(8):
|
| 626 |
+
if s.game == "chess":
|
| 627 |
+
if self._chess_legal(s.board, (sy, sx), (dy, dx), white_turn=False):
|
| 628 |
+
moves.append(((sy, sx), (dy, dx)))
|
| 629 |
+
elif self._checkers_legal(s.board, (sy, sx), (dy, dx), white_turn=False):
|
| 630 |
+
moves.append(((sy, sx), (dy, dx)))
|
| 631 |
+
if not moves:
|
| 632 |
+
await self._finish(ctx, s.players[0], "Bot has no legal moves.")
|
| 633 |
+
return
|
| 634 |
+
src, dst = random.choice(moves)
|
| 635 |
+
sy, sx = src
|
| 636 |
+
dy, dx = dst
|
| 637 |
+
target = s.board[dy][dx]
|
| 638 |
+
if s.game == "checkers":
|
| 639 |
+
self._apply_checkers_capture(s.board, src, dst, white_turn=False)
|
| 640 |
+
s.board[dy][dx] = s.board[sy][sx]
|
| 641 |
+
s.board[sy][sx] = "▫"
|
| 642 |
+
s.turn = 0
|
| 643 |
+
await self._announce(ctx.channel, s, f"Bot played {self._coord_to_text(sy, sx)}{self._coord_to_text(dy, dx)}")
|
| 644 |
+
if s.game == "chess" and target == "♔":
|
| 645 |
+
await self._finish(ctx, 0, "White king captured.")
|
| 646 |
+
|
| 647 |
+
@commands.hybrid_command(name="boardgames", hidden=True, description=get_cmd_desc("commands.boardgames.boardgames_desc"), with_app_command=False)
|
| 648 |
+
async def boardgames(self, ctx: commands.Context) -> None:
|
| 649 |
+
embed = discord.Embed(title="Board Games", color=discord.Color.blurple())
|
| 650 |
+
embed.description = (
|
| 651 |
+
"• chess\n"
|
| 652 |
+
"• checkers\n"
|
| 653 |
+
"• connect4\n"
|
| 654 |
+
"• othello\n\n"
|
| 655 |
+
"Use `/board_start <game> [opponent]` then `/board_move`"
|
| 656 |
+
)
|
| 657 |
+
await ctx.reply(embed=embed)
|
| 658 |
+
|
| 659 |
+
@commands.hybrid_command(name="board_start", hidden=True, description=get_cmd_desc("commands.boardgames.board_start_desc"), with_app_command=False)
|
| 660 |
+
async def board_start(self, ctx: commands.Context, game: str, opponent: discord.Member | None = None) -> None:
|
| 661 |
+
await self.start_game_session(ctx, game, opponent)
|
| 662 |
+
|
| 663 |
+
@commands.hybrid_command(name="chess", hidden=True, description=get_cmd_desc("commands.boardgames.chess_desc"), with_app_command=False)
|
| 664 |
+
async def chess(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 665 |
+
await self.start_game_session(ctx, "chess", opponent)
|
| 666 |
+
|
| 667 |
+
@commands.hybrid_command(name="checkers", hidden=True, description=get_cmd_desc("commands.boardgames.checkers_desc"), with_app_command=False)
|
| 668 |
+
async def checkers(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 669 |
+
await self.start_game_session(ctx, "checkers", opponent)
|
| 670 |
+
|
| 671 |
+
@commands.hybrid_command(name="connect4", hidden=True, description=get_cmd_desc("commands.boardgames.connect4_desc"), with_app_command=False)
|
| 672 |
+
async def connect4(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 673 |
+
await self.start_game_session(ctx, "connect4", opponent)
|
| 674 |
+
|
| 675 |
+
@commands.hybrid_command(name="othello", hidden=True, description=get_cmd_desc("commands.boardgames.othello_desc"), with_app_command=False)
|
| 676 |
+
async def othello(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 677 |
+
await self.start_game_session(ctx, "othello", opponent)
|
| 678 |
+
|
| 679 |
+
async def _play_turn(self, *, guild: discord.Guild, channel: discord.TextChannel, author_id: int, move: str) -> str | None:
|
| 680 |
+
s = self.sessions.get(channel.id)
|
| 681 |
+
if not s:
|
| 682 |
+
return "No active board game in this channel."
|
| 683 |
+
if self._current_player(s) != author_id:
|
| 684 |
+
return "Not your turn."
|
| 685 |
+
|
| 686 |
+
is_first_player = author_id == s.players[0]
|
| 687 |
+
|
| 688 |
+
if s.game == "connect4":
|
| 689 |
+
try:
|
| 690 |
+
col = int(move) - 1
|
| 691 |
+
except ValueError:
|
| 692 |
+
return "Connect4 move is column number 1-7"
|
| 693 |
+
if not 0 <= col < 7:
|
| 694 |
+
return "Column must be 1-7"
|
| 695 |
+
spot = self._connect4_drop(s.board, col, white_turn=is_first_player)
|
| 696 |
+
if not spot:
|
| 697 |
+
return "Column is full."
|
| 698 |
+
if self._has_four(s.board, spot[0], spot[1]):
|
| 699 |
+
await self._announce(channel, s, f"Move: {col + 1}")
|
| 700 |
+
winner = author_id if author_id != 0 else None
|
| 701 |
+
await self._finish_ctxless(guild, channel, winner, "Four in a row!")
|
| 702 |
+
return None
|
| 703 |
+
if all(s.board[0][i] != "⚫" for i in range(7)):
|
| 704 |
+
await self._announce(channel, s, f"Move: {col + 1}")
|
| 705 |
+
await self._finish_ctxless(guild, channel, None, "Board full")
|
| 706 |
+
return None
|
| 707 |
+
s.turn = 1 - s.turn
|
| 708 |
+
await self._announce(channel, s, f"Move: {col + 1}")
|
| 709 |
+
if s.players[1] == 0:
|
| 710 |
+
await self._bot_move_channel(guild, channel)
|
| 711 |
+
return None
|
| 712 |
+
|
| 713 |
+
if s.game == "othello":
|
| 714 |
+
c = self._coord(move)
|
| 715 |
+
if not c:
|
| 716 |
+
return "Use coordinate like d3"
|
| 717 |
+
y, x = c
|
| 718 |
+
black_turn = is_first_player
|
| 719 |
+
flips = self._othello_flips(s.board, y, x, black_turn=black_turn)
|
| 720 |
+
if not flips:
|
| 721 |
+
return "Illegal move."
|
| 722 |
+
s.board[y][x] = "⚫" if black_turn else "⚪"
|
| 723 |
+
for fy, fx in flips:
|
| 724 |
+
s.board[fy][fx] = "⚫" if black_turn else "⚪"
|
| 725 |
+
s.turn = 1 - s.turn
|
| 726 |
+
await self._announce(channel, s, f"Move: {move.lower()}")
|
| 727 |
+
if s.players[1] == 0:
|
| 728 |
+
await self._bot_move_channel(guild, channel)
|
| 729 |
+
return None
|
| 730 |
+
|
| 731 |
+
if len(move) != 4:
|
| 732 |
+
return "Use move format like e2e4"
|
| 733 |
+
src = self._coord(move[:2])
|
| 734 |
+
dst = self._coord(move[2:])
|
| 735 |
+
if not src or not dst:
|
| 736 |
+
return "Invalid coordinates. Example: e2e4"
|
| 737 |
+
|
| 738 |
+
legal = self._chess_legal(s.board, src, dst, white_turn=is_first_player) if s.game == "chess" else self._checkers_legal(s.board, src, dst, white_turn=is_first_player)
|
| 739 |
+
if not legal:
|
| 740 |
+
return "Illegal move."
|
| 741 |
+
|
| 742 |
+
sy, sx = src
|
| 743 |
+
dy, dx = dst
|
| 744 |
+
target = s.board[dy][dx]
|
| 745 |
+
if s.game == "checkers":
|
| 746 |
+
self._apply_checkers_capture(s.board, src, dst, white_turn=is_first_player)
|
| 747 |
+
piece = s.board[sy][sx]
|
| 748 |
+
s.board[dy][dx] = piece
|
| 749 |
+
s.board[sy][sx] = "▫"
|
| 750 |
+
if piece == "♙" and dy == 0:
|
| 751 |
+
s.board[dy][dx] = "♕"
|
| 752 |
+
if piece == "♟" and dy == 7:
|
| 753 |
+
s.board[dy][dx] = "♛"
|
| 754 |
+
|
| 755 |
+
if s.game == "chess" and target in {"♚", "♔"}:
|
| 756 |
+
await self._announce(channel, s, f"Move: {move.lower()}")
|
| 757 |
+
await self._finish_ctxless(guild, channel, author_id, "King captured.")
|
| 758 |
+
return None
|
| 759 |
+
|
| 760 |
+
s.turn = 1 - s.turn
|
| 761 |
+
await self._announce(channel, s, f"Move: {move.lower()}")
|
| 762 |
+
if s.players[1] == 0:
|
| 763 |
+
await self._bot_move_channel(guild, channel)
|
| 764 |
+
return None
|
| 765 |
+
|
| 766 |
+
async def _finish_ctxless(self, guild: discord.Guild, channel: discord.TextChannel, winner_id: int | None, reason: str) -> None:
|
| 767 |
+
s = self.sessions.pop(channel.id, None)
|
| 768 |
+
if not s:
|
| 769 |
+
return
|
| 770 |
+
if winner_id is None:
|
| 771 |
+
await channel.send(f"🤝 Draw | {reason}")
|
| 772 |
+
return
|
| 773 |
+
if winner_id == 0:
|
| 774 |
+
await channel.send(f"🤖 Bot won | {reason}")
|
| 775 |
+
return
|
| 776 |
+
await channel.send(f"🏆 Winner: <@{winner_id}> | {reason}")
|
| 777 |
+
if s.tournament_name:
|
| 778 |
+
await self.bot.db.execute(
|
| 779 |
+
"UPDATE tournaments SET winner_id = ?, status = 'finished' WHERE guild_id = ? AND name = ?",
|
| 780 |
+
winner_id,
|
| 781 |
+
guild.id,
|
| 782 |
+
s.tournament_name,
|
| 783 |
+
)
|
| 784 |
+
|
| 785 |
+
async def _bot_move_channel(self, guild: discord.Guild, channel: discord.TextChannel) -> None:
|
| 786 |
+
class Dummy:
|
| 787 |
+
pass
|
| 788 |
+
d=Dummy(); d.guild=guild; d.channel=channel
|
| 789 |
+
await self._bot_move(d)
|
| 790 |
+
|
| 791 |
+
async def _forfeit(self, *, guild: discord.Guild, channel: discord.TextChannel, author_id: int) -> str | None:
|
| 792 |
+
s = self.sessions.get(channel.id)
|
| 793 |
+
if not s:
|
| 794 |
+
return "No active board game in this channel."
|
| 795 |
+
if author_id not in s.players:
|
| 796 |
+
return "Only players can forfeit."
|
| 797 |
+
winner = s.players[1] if author_id == s.players[0] else s.players[0]
|
| 798 |
+
await self._finish_ctxless(guild, channel, None if winner == 0 else winner, "Forfeit")
|
| 799 |
+
return None
|
| 800 |
+
|
| 801 |
+
@commands.hybrid_command(name="board_move", hidden=True, description=get_cmd_desc("commands.boardgames.board_move_desc"), with_app_command=False)
|
| 802 |
+
async def board_move(self, ctx: commands.Context, move: str) -> None:
|
| 803 |
+
if not ctx.guild or not isinstance(ctx.channel, discord.TextChannel):
|
| 804 |
+
await ctx.reply("Server text channels only.")
|
| 805 |
+
return
|
| 806 |
+
err = await self._play_turn(guild=ctx.guild, channel=ctx.channel, author_id=ctx.author.id, move=move)
|
| 807 |
+
if err:
|
| 808 |
+
await ctx.reply(err)
|
| 809 |
+
|
| 810 |
+
@commands.hybrid_command(name="games_panel", hidden=True, description=get_cmd_desc("commands.boardgames.games_panel_desc"), with_app_command=False)
|
| 811 |
+
async def games_panel(self, ctx: commands.Context) -> None:
|
| 812 |
+
await ctx.reply("<:animatedarrowgreen:1477261279428087979> This panel is deprecated. Use `/gamehub` for the improved game experience.")
|
| 813 |
+
|
| 814 |
+
@commands.hybrid_command(name="board_forfeit", hidden=True, description=get_cmd_desc("commands.boardgames.board_forfeit_desc"), with_app_command=False)
|
| 815 |
+
async def board_forfeit(self, ctx: commands.Context) -> None:
|
| 816 |
+
if not ctx.guild or not isinstance(ctx.channel, discord.TextChannel):
|
| 817 |
+
await ctx.reply("Server text channels only.")
|
| 818 |
+
return
|
| 819 |
+
err = await self._forfeit(guild=ctx.guild, channel=ctx.channel, author_id=ctx.author.id)
|
| 820 |
+
if err:
|
| 821 |
+
await ctx.reply(err)
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
async def setup(bot: commands.Bot) -> None:
|
| 825 |
+
await bot.add_cog(BoardGames(bot))
|
bot/cogs/community.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bot/cogs/configuration.py
CHANGED
|
@@ -1,636 +1,637 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import datetime as dt
|
| 4 |
-
import re
|
| 5 |
-
|
| 6 |
-
import discord
|
| 7 |
-
from discord.ext import commands
|
| 8 |
-
|
| 9 |
-
from bot.theme import fancy_header, NEON_CYAN, NEON_PURPLE
|
| 10 |
-
from bot.
|
| 11 |
-
from bot.emojis import ui
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
await
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
await
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
await
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
await
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
await
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
view
|
| 194 |
-
|
| 195 |
-
await
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
self.
|
| 203 |
-
self.
|
| 204 |
-
self.
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
self.
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
self.
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
self.
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
self.
|
| 255 |
-
self.
|
| 256 |
-
self.
|
| 257 |
-
self.
|
| 258 |
-
self.
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
"
|
| 264 |
-
|
| 265 |
-
self.
|
| 266 |
-
self.
|
| 267 |
-
|
| 268 |
-
self.
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
discord.SelectOption(label="
|
| 278 |
-
discord.SelectOption(label="
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
await
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
discord.SelectOption(label="
|
| 308 |
-
discord.SelectOption(label="@
|
| 309 |
-
discord.SelectOption(label="
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
await
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
"
|
| 331 |
-
"
|
| 332 |
-
"
|
| 333 |
-
"
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
(
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
embed.
|
| 370 |
-
embed.add_field(name="
|
| 371 |
-
embed.add_field(name="
|
| 372 |
-
embed.add_field(name="
|
| 373 |
-
embed.add_field(name="Verification
|
| 374 |
-
embed.add_field(name="
|
| 375 |
-
embed.add_field(name="
|
| 376 |
-
embed.add_field(name="Daily
|
| 377 |
-
embed.add_field(name="Daily
|
| 378 |
-
embed.add_field(name="Daily
|
| 379 |
-
embed.add_field(name="Daily
|
| 380 |
-
embed.add_field(name="Daily
|
| 381 |
-
embed.add_field(name="Daily
|
| 382 |
-
embed.add_field(name="Daily
|
| 383 |
-
embed.add_field(name="
|
| 384 |
-
embed.add_field(name="
|
| 385 |
-
embed.add_field(name="Free Games
|
| 386 |
-
embed.add_field(name="
|
| 387 |
-
embed.add_field(name="Support AI
|
| 388 |
-
embed.add_field(name="
|
| 389 |
-
embed.add_field(name="Wisdom
|
| 390 |
-
embed.add_field(name="
|
| 391 |
-
embed.add_field(name="Game News
|
| 392 |
-
embed.
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
@commands.
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
"
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
view
|
| 539 |
-
|
| 540 |
-
await
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
"
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
"
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
"
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
"
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
@commands.
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
"
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
f"
|
| 627 |
-
"
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import datetime as dt
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
import discord
|
| 7 |
+
from discord.ext import commands
|
| 8 |
+
|
| 9 |
+
from bot.theme import fancy_header, NEON_CYAN, NEON_PURPLE
|
| 10 |
+
from bot.i18n import get_cmd_desc
|
| 11 |
+
from bot.emojis import ui
|
| 12 |
+
from bot.emojis import ui
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ConfigPanelView(discord.ui.View):
|
| 16 |
+
def __init__(self, cog: "Configuration") -> None:
|
| 17 |
+
super().__init__(timeout=None)
|
| 18 |
+
self.cog = cog
|
| 19 |
+
|
| 20 |
+
@discord.ui.button(label="Refresh Panel", style=discord.ButtonStyle.blurple, emoji=ui("refresh"), row=3, custom_id="config_refresh")
|
| 21 |
+
async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 22 |
+
if not interaction.guild:
|
| 23 |
+
await interaction.response.send_message("This button works only in servers.", ephemeral=True)
|
| 24 |
+
return
|
| 25 |
+
embed = await self.cog.build_config_embed(interaction.guild)
|
| 26 |
+
await interaction.response.edit_message(embed=embed, view=self)
|
| 27 |
+
|
| 28 |
+
@discord.ui.button(label="Toggle AutoMod", style=discord.ButtonStyle.danger, emoji="🛡️", row=0, custom_id="config_automod")
|
| 29 |
+
async def toggle_automod(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 30 |
+
if not interaction.guild:
|
| 31 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 32 |
+
return
|
| 33 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 34 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 35 |
+
return
|
| 36 |
+
row = await self.cog.bot.db.fetchone("SELECT automod_enabled FROM guild_config WHERE guild_id = ?", interaction.guild.id)
|
| 37 |
+
current = row[0] if row else 0
|
| 38 |
+
new_val = 0 if current else 1
|
| 39 |
+
await self.cog.bot.db.execute(
|
| 40 |
+
"INSERT INTO guild_config(guild_id, automod_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET automod_enabled = excluded.automod_enabled",
|
| 41 |
+
interaction.guild.id, new_val,
|
| 42 |
+
)
|
| 43 |
+
state = "enabled" if new_val else "disabled"
|
| 44 |
+
await interaction.response.send_message(f"🛡️ AutoMod {state}.", ephemeral=True)
|
| 45 |
+
embed = await self.cog.build_config_embed(interaction.guild)
|
| 46 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 47 |
+
|
| 48 |
+
@discord.ui.button(label="Toggle Daily", style=discord.ButtonStyle.success, emoji=ui("calendar"), row=0, custom_id="config_daily")
|
| 49 |
+
async def toggle_daily(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 50 |
+
if not interaction.guild:
|
| 51 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 52 |
+
return
|
| 53 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 54 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 55 |
+
return
|
| 56 |
+
row = await self.cog.bot.db.fetchone("SELECT daily_enabled FROM guild_config WHERE guild_id = ?", interaction.guild.id)
|
| 57 |
+
current = row[0] if row else 0
|
| 58 |
+
new_val = 0 if current else 1
|
| 59 |
+
await self.cog.bot.db.execute(
|
| 60 |
+
"INSERT INTO guild_config(guild_id, daily_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_enabled = excluded.daily_enabled",
|
| 61 |
+
interaction.guild.id, new_val,
|
| 62 |
+
)
|
| 63 |
+
state = "enabled" if new_val else "disabled"
|
| 64 |
+
await interaction.response.send_message(f"📅 Daily message {state}.", ephemeral=True)
|
| 65 |
+
embed = await self.cog.build_config_embed(interaction.guild)
|
| 66 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 67 |
+
|
| 68 |
+
@discord.ui.button(label="Toggle Support AI", style=discord.ButtonStyle.primary, emoji=ui("support"), row=0, custom_id="config_support_ai")
|
| 69 |
+
async def toggle_support_ai(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 70 |
+
if not interaction.guild:
|
| 71 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 72 |
+
return
|
| 73 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 74 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 75 |
+
return
|
| 76 |
+
row = await self.cog.bot.db.fetchone("SELECT support_ai_enabled FROM guild_config WHERE guild_id = ?", interaction.guild.id)
|
| 77 |
+
current = row[0] if row else 0
|
| 78 |
+
new_val = 0 if current else 1
|
| 79 |
+
await self.cog.bot.db.execute(
|
| 80 |
+
"INSERT INTO guild_config(guild_id, support_ai_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET support_ai_enabled = excluded.support_ai_enabled",
|
| 81 |
+
interaction.guild.id, new_val,
|
| 82 |
+
)
|
| 83 |
+
state = "enabled" if new_val else "disabled"
|
| 84 |
+
await interaction.response.send_message(f"🛟 Support AI {state}.", ephemeral=True)
|
| 85 |
+
embed = await self.cog.build_config_embed(interaction.guild)
|
| 86 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 87 |
+
|
| 88 |
+
@discord.ui.button(label="Toggle Wisdom", style=discord.ButtonStyle.secondary, emoji=ui("brain"), row=0, custom_id="config_wisdom")
|
| 89 |
+
async def toggle_wisdom(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 90 |
+
if not interaction.guild:
|
| 91 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 92 |
+
return
|
| 93 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 94 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 95 |
+
return
|
| 96 |
+
row = await self.cog.bot.db.fetchone("SELECT wisdom_enabled FROM guild_config WHERE guild_id = ?", interaction.guild.id)
|
| 97 |
+
current = row[0] if row else 0
|
| 98 |
+
new_val = 0 if current else 1
|
| 99 |
+
await self.cog.bot.db.execute(
|
| 100 |
+
"INSERT INTO guild_config(guild_id, wisdom_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET wisdom_enabled = excluded.wisdom_enabled",
|
| 101 |
+
interaction.guild.id, new_val,
|
| 102 |
+
)
|
| 103 |
+
state = "enabled" if new_val else "disabled"
|
| 104 |
+
await interaction.response.send_message(f"🧠 Daily wisdom {state}.", ephemeral=True)
|
| 105 |
+
embed = await self.cog.build_config_embed(interaction.guild)
|
| 106 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 107 |
+
|
| 108 |
+
@discord.ui.button(label="Set Log Channel", style=discord.ButtonStyle.blurple, emoji="📝", row=1, custom_id="config_log")
|
| 109 |
+
async def set_log(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 110 |
+
if not interaction.guild:
|
| 111 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 112 |
+
return
|
| 113 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 114 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 115 |
+
return
|
| 116 |
+
await interaction.response.send_message(
|
| 117 |
+
"Select a text channel for logging:",
|
| 118 |
+
view=ChannelSelectModal(self.cog, interaction.guild.id, "log_channel_id", "📝 Log channel set to"),
|
| 119 |
+
ephemeral=True,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
@discord.ui.button(label="Set Welcome Channel", style=discord.ButtonStyle.blurple, emoji="👋", row=1, custom_id="config_welcome")
|
| 123 |
+
async def set_welcome(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 124 |
+
if not interaction.guild:
|
| 125 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 126 |
+
return
|
| 127 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 128 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 129 |
+
return
|
| 130 |
+
await interaction.response.send_message(
|
| 131 |
+
"Select a text channel for welcomes:",
|
| 132 |
+
view=ChannelSelectModal(self.cog, interaction.guild.id, "welcome_channel_id", "👋 Welcome channel set to"),
|
| 133 |
+
ephemeral=True,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
@discord.ui.button(label="Set Daily Channel", style=discord.ButtonStyle.blurple, emoji="📨", row=1, custom_id="config_daily_ch")
|
| 137 |
+
async def set_daily_ch(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 138 |
+
if not interaction.guild:
|
| 139 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 140 |
+
return
|
| 141 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 142 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 143 |
+
return
|
| 144 |
+
await interaction.response.send_message(
|
| 145 |
+
"Select a text channel for daily messages:",
|
| 146 |
+
view=ChannelSelectModal(self.cog, interaction.guild.id, "daily_channel_id", "📨 Daily channel set to"),
|
| 147 |
+
ephemeral=True,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
@discord.ui.button(label="Set Poll Channel", style=discord.ButtonStyle.blurple, emoji="📊", row=1, custom_id="config_poll_ch")
|
| 151 |
+
async def set_poll_ch(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 152 |
+
if not interaction.guild:
|
| 153 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 154 |
+
return
|
| 155 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 156 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 157 |
+
return
|
| 158 |
+
await interaction.response.send_message(
|
| 159 |
+
"Select a text channel for polls:",
|
| 160 |
+
view=ChannelSelectModal(self.cog, interaction.guild.id, "poll_channel_id", "📊 Poll channel set to"),
|
| 161 |
+
ephemeral=True,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
@discord.ui.button(label="Set Daily Message", style=discord.ButtonStyle.gray, emoji="✏️", row=2, custom_id="config_daily_msg")
|
| 165 |
+
async def set_daily_msg(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 166 |
+
if not interaction.guild:
|
| 167 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 168 |
+
return
|
| 169 |
+
if not interaction.user.guild_permissions.manage_guild:
|
| 170 |
+
await interaction.response.send_message("Manage Server permission required.", ephemeral=True)
|
| 171 |
+
return
|
| 172 |
+
await interaction.response.send_modal(DailyMessageModal(self.cog, interaction.guild.id))
|
| 173 |
+
|
| 174 |
+
@discord.ui.button(label="Test Daily", style=discord.ButtonStyle.gray, emoji="🧪", row=2, custom_id="config_test_daily")
|
| 175 |
+
async def test_daily(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 176 |
+
if not interaction.guild:
|
| 177 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 178 |
+
return
|
| 179 |
+
row = await self.cog.bot.db.fetchone(
|
| 180 |
+
"SELECT daily_channel_id, daily_message, daily_title, daily_image_url, daily_button_label, daily_button_url FROM guild_config WHERE guild_id = ?",
|
| 181 |
+
interaction.guild.id,
|
| 182 |
+
)
|
| 183 |
+
if not row or not row[0]:
|
| 184 |
+
await interaction.response.send_message("Set a daily channel first.", ephemeral=True)
|
| 185 |
+
return
|
| 186 |
+
channel = interaction.guild.get_channel(row[0])
|
| 187 |
+
if not channel:
|
| 188 |
+
await interaction.response.send_message("Configured daily channel no longer exists.", ephemeral=True)
|
| 189 |
+
return
|
| 190 |
+
embed = self.cog._build_daily_embed(interaction.guild.name, row[2] or "", row[1] or "", row[3])
|
| 191 |
+
view = None
|
| 192 |
+
if row[4] and row[5]:
|
| 193 |
+
view = discord.ui.View()
|
| 194 |
+
view.add_item(discord.ui.Button(label=row[4], url=row[5]))
|
| 195 |
+
await channel.send(embed=embed, view=view)
|
| 196 |
+
await interaction.response.send_message(f"✅ Sent test daily message to {channel.mention}", ephemeral=True)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class ChannelSelectModal(discord.ui.View):
|
| 200 |
+
def __init__(self, cog: "Configuration", guild_id: int, column: str, success_prefix: str) -> None:
|
| 201 |
+
super().__init__(timeout=300)
|
| 202 |
+
self.cog = cog
|
| 203 |
+
self.guild_id = guild_id
|
| 204 |
+
self.column = column
|
| 205 |
+
self.success_prefix = success_prefix
|
| 206 |
+
|
| 207 |
+
self.channel_select = discord.ui.ChannelSelect(
|
| 208 |
+
cls=discord.ui.ChannelSelect,
|
| 209 |
+
channel_types=[discord.ChannelType.text],
|
| 210 |
+
placeholder="Select a text channel...",
|
| 211 |
+
min_values=1,
|
| 212 |
+
max_values=1,
|
| 213 |
+
custom_id="channel_select_modal",
|
| 214 |
+
)
|
| 215 |
+
self.channel_select.callback = self._channel_select_callback
|
| 216 |
+
self.add_item(self.channel_select)
|
| 217 |
+
|
| 218 |
+
async def _channel_select_callback(self, interaction: discord.Interaction) -> None:
|
| 219 |
+
channel = self.channel_select.values[0]
|
| 220 |
+
await self.cog.bot.db.execute(
|
| 221 |
+
f"INSERT INTO guild_config(guild_id, {self.column}) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET {self.column} = excluded.{self.column}",
|
| 222 |
+
self.guild_id, channel.id,
|
| 223 |
+
)
|
| 224 |
+
await interaction.response.send_message(f"{self.success_prefix} {channel.mention}", ephemeral=True)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class DailyMessageModal(discord.ui.Modal, title="✏️ Set Daily Message"):
|
| 228 |
+
message = discord.ui.TextInput(
|
| 229 |
+
label="Daily Message Text",
|
| 230 |
+
placeholder="Your daily message here...",
|
| 231 |
+
required=True,
|
| 232 |
+
style=discord.TextStyle.paragraph,
|
| 233 |
+
max_length=500,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
def __init__(self, cog: "Configuration", guild_id: int) -> None:
|
| 237 |
+
super().__init__(timeout=None)
|
| 238 |
+
self.cog = cog
|
| 239 |
+
self.guild_id = guild_id
|
| 240 |
+
|
| 241 |
+
async def on_submit(self, interaction: discord.Interaction) -> None:
|
| 242 |
+
await self.cog.bot.db.execute(
|
| 243 |
+
"INSERT INTO guild_config(guild_id, daily_message) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_message = excluded.daily_message",
|
| 244 |
+
self.guild_id, str(self.message.value),
|
| 245 |
+
)
|
| 246 |
+
await interaction.response.send_message("🗓️ Daily message text updated.", ephemeral=True)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
class FreeGameSetupView(discord.ui.View):
|
| 252 |
+
def __init__(self, cog: "Configuration", guild_id: int, channel_id: int, role_id: int | None = None) -> None:
|
| 253 |
+
super().__init__(timeout=300)
|
| 254 |
+
self.cog = cog
|
| 255 |
+
self.guild_id = guild_id
|
| 256 |
+
self.channel_id = channel_id
|
| 257 |
+
self.role_id = role_id
|
| 258 |
+
self.mention_type = "role"
|
| 259 |
+
self.platforms = {"epic", "steam", "gog"}
|
| 260 |
+
|
| 261 |
+
async def _save(self, interaction: discord.Interaction) -> None:
|
| 262 |
+
await self.cog.bot.db.execute(
|
| 263 |
+
"INSERT INTO guild_config(guild_id, free_games_channel_id, free_games_role_id, free_games_platforms, free_games_mention_type) VALUES (?, ?, ?, ?, ?) "
|
| 264 |
+
"ON CONFLICT(guild_id) DO UPDATE SET free_games_channel_id=excluded.free_games_channel_id, free_games_role_id=excluded.free_games_role_id, free_games_platforms=excluded.free_games_platforms, free_games_mention_type=excluded.free_games_mention_type",
|
| 265 |
+
self.guild_id,
|
| 266 |
+
self.channel_id,
|
| 267 |
+
self.role_id,
|
| 268 |
+
",".join(sorted(self.platforms)),
|
| 269 |
+
self.mention_type,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
@discord.ui.select(
|
| 273 |
+
placeholder="Choose stores for alerts",
|
| 274 |
+
min_values=1,
|
| 275 |
+
max_values=3,
|
| 276 |
+
options=[
|
| 277 |
+
discord.SelectOption(label="Epic Games", value="epic", emoji="<:animatedarrowgreen:1477261279428087979>"),
|
| 278 |
+
discord.SelectOption(label="Steam", value="steam", emoji="<:animatedarrowgreen:1477261279428087979>"),
|
| 279 |
+
discord.SelectOption(label="GOG", value="gog", emoji="<:animatedarrowgreen:1477261279428087979>"),
|
| 280 |
+
],
|
| 281 |
+
)
|
| 282 |
+
async def stores(self, interaction: discord.Interaction, select: discord.ui.Select) -> None:
|
| 283 |
+
self.platforms = set(select.values)
|
| 284 |
+
await self._save(interaction)
|
| 285 |
+
await interaction.response.send_message("<:animatedarrowgreen:1477261279428087979> Free games stores updated.", ephemeral=True)
|
| 286 |
+
|
| 287 |
+
@discord.ui.select(
|
| 288 |
+
placeholder="Choose ping role (optional)",
|
| 289 |
+
min_values=0,
|
| 290 |
+
max_values=1,
|
| 291 |
+
cls=discord.ui.RoleSelect,
|
| 292 |
+
)
|
| 293 |
+
async def role_select(self, interaction: discord.Interaction, select: discord.ui.RoleSelect) -> None:
|
| 294 |
+
role = select.values[0] if select.values else None
|
| 295 |
+
self.role_id = role.id if role else None
|
| 296 |
+
await self._save(interaction)
|
| 297 |
+
if role:
|
| 298 |
+
await interaction.response.send_message(f"✅ Free games role set to {role.mention}", ephemeral=True)
|
| 299 |
+
else:
|
| 300 |
+
await interaction.response.send_message("✅ Free games role cleared.", ephemeral=True)
|
| 301 |
+
|
| 302 |
+
@discord.ui.select(
|
| 303 |
+
placeholder="Mention style",
|
| 304 |
+
min_values=1,
|
| 305 |
+
max_values=1,
|
| 306 |
+
options=[
|
| 307 |
+
discord.SelectOption(label="Role mention", value="role", emoji="🏷️"),
|
| 308 |
+
discord.SelectOption(label="@everyone", value="everyone", emoji="📢"),
|
| 309 |
+
discord.SelectOption(label="@here", value="here", emoji="📣"),
|
| 310 |
+
discord.SelectOption(label="No mention", value="none", emoji="🔕"),
|
| 311 |
+
],
|
| 312 |
+
)
|
| 313 |
+
async def mention_select(self, interaction: discord.Interaction, select: discord.ui.Select) -> None:
|
| 314 |
+
self.mention_type = (select.values[0] if select.values else "role").strip().lower()
|
| 315 |
+
await self._save(interaction)
|
| 316 |
+
await interaction.response.send_message(f"✅ Mention type set to `{self.mention_type}`", ephemeral=True)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
class Configuration(commands.Cog):
|
| 320 |
+
TIME_RE = re.compile(r"^(?:[01]\d|2[0-3]):[0-5]\d$")
|
| 321 |
+
|
| 322 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 323 |
+
self.bot = bot
|
| 324 |
+
|
| 325 |
+
async def cog_load(self) -> None:
|
| 326 |
+
self.bot.add_view(ConfigPanelView(self))
|
| 327 |
+
|
| 328 |
+
async def build_config_embed(self, guild: discord.Guild) -> discord.Embed:
|
| 329 |
+
row = await self.bot.db.fetchone(
|
| 330 |
+
"SELECT log_channel_id, welcome_channel_id, suggestion_channel_id, automod_enabled, daily_channel_id, "
|
| 331 |
+
"daily_enabled, daily_message, daily_time, daily_utc_offset, verify_channel_id, verify_role_id, "
|
| 332 |
+
"daily_title, daily_image_url, daily_button_label, daily_button_url, poll_channel_id, free_games_channel_id, free_games_role_id, "
|
| 333 |
+
"support_channel_id, support_ai_enabled, wisdom_channel_id, wisdom_enabled, game_news_channel_id, game_news_role_id "
|
| 334 |
+
"FROM guild_config WHERE guild_id = ?",
|
| 335 |
+
guild.id,
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
values = row if row else (None, None, None, 0, None, 0, "Not set", "09:00", 0, None, None, "🌅 Daily Message", None, None, None, None, None, None, None, 0, None, 0, None, None)
|
| 339 |
+
(
|
| 340 |
+
log_id,
|
| 341 |
+
welcome_id,
|
| 342 |
+
suggestion_id,
|
| 343 |
+
automod_enabled,
|
| 344 |
+
daily_channel_id,
|
| 345 |
+
daily_enabled,
|
| 346 |
+
daily_message,
|
| 347 |
+
daily_time,
|
| 348 |
+
daily_utc_offset,
|
| 349 |
+
verify_channel_id,
|
| 350 |
+
verify_role_id,
|
| 351 |
+
daily_title,
|
| 352 |
+
daily_image_url,
|
| 353 |
+
daily_button_label,
|
| 354 |
+
daily_button_url,
|
| 355 |
+
poll_channel_id,
|
| 356 |
+
free_games_channel_id,
|
| 357 |
+
free_games_role_id,
|
| 358 |
+
support_channel_id,
|
| 359 |
+
support_ai_enabled,
|
| 360 |
+
wisdom_channel_id,
|
| 361 |
+
wisdom_enabled,
|
| 362 |
+
game_news_channel_id,
|
| 363 |
+
game_news_role_id,
|
| 364 |
+
) = values
|
| 365 |
+
|
| 366 |
+
def mention(channel_id: int | None) -> str:
|
| 367 |
+
return f"<#{channel_id}>" if channel_id else "Not set"
|
| 368 |
+
|
| 369 |
+
embed = discord.Embed(title=fancy_header(f"Settings • {guild.name}"), color=NEON_CYAN)
|
| 370 |
+
embed.add_field(name="Log Channel", value=mention(log_id), inline=True)
|
| 371 |
+
embed.add_field(name="Welcome Channel", value=mention(welcome_id), inline=True)
|
| 372 |
+
embed.add_field(name="Suggestion Channel", value=mention(suggestion_id), inline=True)
|
| 373 |
+
embed.add_field(name="Verification Channel", value=mention(verify_channel_id), inline=True)
|
| 374 |
+
embed.add_field(name="Verification Role", value=f"<@&{verify_role_id}>" if verify_role_id else "Not set", inline=True)
|
| 375 |
+
embed.add_field(name="AutoMod", value="✅ On" if automod_enabled else "❌ Off", inline=True)
|
| 376 |
+
embed.add_field(name="Daily Channel", value=mention(daily_channel_id), inline=True)
|
| 377 |
+
embed.add_field(name="Daily Enabled", value="✅ On" if daily_enabled else "❌ Off", inline=True)
|
| 378 |
+
embed.add_field(name="Daily Time", value=f"`{daily_time or '09:00'}`", inline=True)
|
| 379 |
+
embed.add_field(name="Daily UTC Offset", value=f"`UTC{daily_utc_offset:+d}`", inline=True)
|
| 380 |
+
embed.add_field(name="Daily Title", value=(daily_title or "🌅 Daily Message")[:128], inline=False)
|
| 381 |
+
embed.add_field(name="Daily Message Text", value=(daily_message or "Not set")[:1024], inline=False)
|
| 382 |
+
embed.add_field(name="Daily Image URL", value=daily_image_url or "Not set", inline=False)
|
| 383 |
+
embed.add_field(name="Daily Button", value=f"{daily_button_label or 'Not set'} | {daily_button_url or 'Not set'}", inline=False)
|
| 384 |
+
embed.add_field(name="Poll Channel", value=mention(poll_channel_id), inline=True)
|
| 385 |
+
embed.add_field(name="Free Games Channel", value=mention(free_games_channel_id), inline=True)
|
| 386 |
+
embed.add_field(name="Free Games Role", value=f"<@&{free_games_role_id}>" if free_games_role_id else "Not set", inline=True)
|
| 387 |
+
embed.add_field(name="Support AI Channel", value=mention(support_channel_id), inline=True)
|
| 388 |
+
embed.add_field(name="Support AI Enabled", value="✅ On" if support_ai_enabled else "❌ Off", inline=True)
|
| 389 |
+
embed.add_field(name="Wisdom Channel", value=mention(wisdom_channel_id), inline=True)
|
| 390 |
+
embed.add_field(name="Wisdom Daily", value="✅ On" if wisdom_enabled else "❌ Off", inline=True)
|
| 391 |
+
embed.add_field(name="Game News Channel", value=mention(game_news_channel_id), inline=True)
|
| 392 |
+
embed.add_field(name="Game News Role", value=f"<@&{game_news_role_id}>" if game_news_role_id else "Not set", inline=True)
|
| 393 |
+
embed.set_footer(text="Use /set commands to update • Click Refresh to update panel")
|
| 394 |
+
return embed
|
| 395 |
+
|
| 396 |
+
def _build_daily_embed(self, guild_name: str, title: str, message_text: str, image_url: str | None) -> discord.Embed:
|
| 397 |
+
today = dt.datetime.utcnow().date().isoformat()
|
| 398 |
+
embed = discord.Embed(
|
| 399 |
+
title=title or "🌅 Daily Message",
|
| 400 |
+
description=message_text or "Have a productive and positive day!",
|
| 401 |
+
color=discord.Color.gold(),
|
| 402 |
+
)
|
| 403 |
+
if image_url:
|
| 404 |
+
embed.set_image(url=image_url)
|
| 405 |
+
embed.set_footer(text=f"{guild_name} • {today}")
|
| 406 |
+
return embed
|
| 407 |
+
|
| 408 |
+
@commands.hybrid_group(name="set", fallback="show")
|
| 409 |
+
@commands.has_permissions(manage_guild=True)
|
| 410 |
+
async def setgroup(self, ctx: commands.Context) -> None:
|
| 411 |
+
embed = await self.build_config_embed(ctx.guild)
|
| 412 |
+
await ctx.reply(embed=embed, view=ConfigPanelView(self))
|
| 413 |
+
|
| 414 |
+
@setgroup.command(name="log")
|
| 415 |
+
async def set_log(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
|
| 416 |
+
await self.bot.db.execute(
|
| 417 |
+
"INSERT INTO guild_config(guild_id, log_channel_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET log_channel_id = excluded.log_channel_id",
|
| 418 |
+
ctx.guild.id,
|
| 419 |
+
channel.id,
|
| 420 |
+
)
|
| 421 |
+
await ctx.reply(f"📝 Log channel set to {channel.mention}")
|
| 422 |
+
|
| 423 |
+
@setgroup.command(name="welcome")
|
| 424 |
+
async def set_welcome(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
|
| 425 |
+
await self.bot.db.execute(
|
| 426 |
+
"INSERT INTO guild_config(guild_id, welcome_channel_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET welcome_channel_id = excluded.welcome_channel_id",
|
| 427 |
+
ctx.guild.id,
|
| 428 |
+
channel.id,
|
| 429 |
+
)
|
| 430 |
+
await ctx.reply(f"👋 Welcome channel set to {channel.mention}")
|
| 431 |
+
|
| 432 |
+
@setgroup.command(name="suggestions")
|
| 433 |
+
async def set_suggestion(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
|
| 434 |
+
await self.bot.db.execute(
|
| 435 |
+
"INSERT INTO guild_config(guild_id, suggestion_channel_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET suggestion_channel_id = excluded.suggestion_channel_id",
|
| 436 |
+
ctx.guild.id,
|
| 437 |
+
channel.id,
|
| 438 |
+
)
|
| 439 |
+
await ctx.reply(f"💡 Suggestion channel set to {channel.mention}")
|
| 440 |
+
|
| 441 |
+
@setgroup.command(name="automod")
|
| 442 |
+
async def set_automod(self, ctx: commands.Context, enabled: bool) -> None:
|
| 443 |
+
await self.bot.db.execute(
|
| 444 |
+
"INSERT INTO guild_config(guild_id, automod_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET automod_enabled = excluded.automod_enabled",
|
| 445 |
+
ctx.guild.id,
|
| 446 |
+
1 if enabled else 0,
|
| 447 |
+
)
|
| 448 |
+
await ctx.reply(f"🛡️ AutoMod {'enabled' if enabled else 'disabled'}")
|
| 449 |
+
|
| 450 |
+
@setgroup.command(name="dailychannel")
|
| 451 |
+
async def set_daily_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
|
| 452 |
+
await self.bot.db.execute(
|
| 453 |
+
"INSERT INTO guild_config(guild_id, daily_channel_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_channel_id = excluded.daily_channel_id",
|
| 454 |
+
ctx.guild.id,
|
| 455 |
+
channel.id,
|
| 456 |
+
)
|
| 457 |
+
await ctx.reply(f"📨 Daily message channel set to {channel.mention}")
|
| 458 |
+
|
| 459 |
+
@setgroup.command(name="dailymessage")
|
| 460 |
+
async def set_daily_message(self, ctx: commands.Context, *, text: str) -> None:
|
| 461 |
+
await self.bot.db.execute(
|
| 462 |
+
"INSERT INTO guild_config(guild_id, daily_message) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_message = excluded.daily_message",
|
| 463 |
+
ctx.guild.id,
|
| 464 |
+
text,
|
| 465 |
+
)
|
| 466 |
+
await ctx.reply("🗓️ Daily message text updated.")
|
| 467 |
+
|
| 468 |
+
@setgroup.command(name="dailytitle")
|
| 469 |
+
async def set_daily_title(self, ctx: commands.Context, *, title: str) -> None:
|
| 470 |
+
await self.bot.db.execute(
|
| 471 |
+
"INSERT INTO guild_config(guild_id, daily_title) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_title = excluded.daily_title",
|
| 472 |
+
ctx.guild.id,
|
| 473 |
+
title,
|
| 474 |
+
)
|
| 475 |
+
await ctx.reply("🖋️ Daily message title updated.")
|
| 476 |
+
|
| 477 |
+
@setgroup.command(name="dailyimage")
|
| 478 |
+
async def set_daily_image(self, ctx: commands.Context, image_url: str = "") -> None:
|
| 479 |
+
await self.bot.db.execute(
|
| 480 |
+
"INSERT INTO guild_config(guild_id, daily_image_url) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_image_url = excluded.daily_image_url",
|
| 481 |
+
ctx.guild.id,
|
| 482 |
+
image_url or None,
|
| 483 |
+
)
|
| 484 |
+
await ctx.reply("🖼️ Daily image updated (empty value removes it).")
|
| 485 |
+
|
| 486 |
+
@setgroup.command(name="dailybutton")
|
| 487 |
+
async def set_daily_button(self, ctx: commands.Context, label: str = "", url: str = "") -> None:
|
| 488 |
+
await self.bot.db.execute(
|
| 489 |
+
"INSERT INTO guild_config(guild_id, daily_button_label, daily_button_url) VALUES (?, ?, ?) "
|
| 490 |
+
"ON CONFLICT(guild_id) DO UPDATE SET daily_button_label = excluded.daily_button_label, daily_button_url = excluded.daily_button_url",
|
| 491 |
+
ctx.guild.id,
|
| 492 |
+
label or None,
|
| 493 |
+
url or None,
|
| 494 |
+
)
|
| 495 |
+
await ctx.reply("🔗 Daily button settings updated.")
|
| 496 |
+
|
| 497 |
+
@setgroup.command(name="dailytime")
|
| 498 |
+
async def set_daily_time(self, ctx: commands.Context, hhmm: str) -> None:
|
| 499 |
+
hhmm = hhmm.strip()
|
| 500 |
+
if not self.TIME_RE.fullmatch(hhmm):
|
| 501 |
+
await ctx.reply("Use time in 24h format `HH:MM` (example: `18:30`).")
|
| 502 |
+
return
|
| 503 |
+
await self.bot.db.execute(
|
| 504 |
+
"INSERT INTO guild_config(guild_id, daily_time) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_time = excluded.daily_time",
|
| 505 |
+
ctx.guild.id,
|
| 506 |
+
hhmm,
|
| 507 |
+
)
|
| 508 |
+
await ctx.reply(f"⏰ Daily message time set to `{hhmm}`")
|
| 509 |
+
|
| 510 |
+
@setgroup.command(name="dailyutc")
|
| 511 |
+
async def set_daily_utc(self, ctx: commands.Context, offset: int) -> None:
|
| 512 |
+
if offset < -12 or offset > 14:
|
| 513 |
+
await ctx.reply("UTC offset must be between `-12` and `+14`.")
|
| 514 |
+
return
|
| 515 |
+
await self.bot.db.execute(
|
| 516 |
+
"INSERT INTO guild_config(guild_id, daily_utc_offset) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_utc_offset = excluded.daily_utc_offset",
|
| 517 |
+
ctx.guild.id,
|
| 518 |
+
offset,
|
| 519 |
+
)
|
| 520 |
+
await ctx.reply(f"🌍 Daily timezone set to `UTC{offset:+d}`")
|
| 521 |
+
|
| 522 |
+
@setgroup.command(name="dailytest")
|
| 523 |
+
async def set_daily_test(self, ctx: commands.Context) -> None:
|
| 524 |
+
row = await self.bot.db.fetchone(
|
| 525 |
+
"SELECT daily_channel_id, daily_message, daily_title, daily_image_url, daily_button_label, daily_button_url FROM guild_config WHERE guild_id = ?",
|
| 526 |
+
ctx.guild.id,
|
| 527 |
+
)
|
| 528 |
+
if not row or not row[0]:
|
| 529 |
+
await ctx.reply("Set a daily channel first with `/set dailychannel`.")
|
| 530 |
+
return
|
| 531 |
+
channel = ctx.guild.get_channel(row[0])
|
| 532 |
+
if not channel:
|
| 533 |
+
await ctx.reply("Configured daily channel no longer exists.")
|
| 534 |
+
return
|
| 535 |
+
embed = self._build_daily_embed(ctx.guild.name, row[2] or "", row[1] or "", row[3])
|
| 536 |
+
view = None
|
| 537 |
+
if row[4] and row[5]:
|
| 538 |
+
view = discord.ui.View()
|
| 539 |
+
view.add_item(discord.ui.Button(label=row[4], url=row[5]))
|
| 540 |
+
await channel.send(embed=embed, view=view)
|
| 541 |
+
await ctx.reply(f"✅ Sent test daily message to {channel.mention}")
|
| 542 |
+
|
| 543 |
+
@setgroup.command(name="dailytoggle")
|
| 544 |
+
async def set_daily_toggle(self, ctx: commands.Context, enabled: bool) -> None:
|
| 545 |
+
await self.bot.db.execute(
|
| 546 |
+
"INSERT INTO guild_config(guild_id, daily_enabled) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET daily_enabled = excluded.daily_enabled",
|
| 547 |
+
ctx.guild.id,
|
| 548 |
+
1 if enabled else 0,
|
| 549 |
+
)
|
| 550 |
+
await ctx.reply(f"📅 Daily message {'enabled' if enabled else 'disabled'}.")
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
@setgroup.command(name="pollchannel")
|
| 554 |
+
async def set_poll_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None:
|
| 555 |
+
await self.bot.db.execute(
|
| 556 |
+
"INSERT INTO guild_config(guild_id, poll_channel_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET poll_channel_id = excluded.poll_channel_id",
|
| 557 |
+
ctx.guild.id,
|
| 558 |
+
channel.id,
|
| 559 |
+
)
|
| 560 |
+
await ctx.reply(f"📊 Poll channel set to {channel.mention}")
|
| 561 |
+
|
| 562 |
+
@setgroup.command(name="freegames")
|
| 563 |
+
async def set_free_games_channel(self, ctx: commands.Context, channel: discord.TextChannel, role: discord.Role | None = None) -> None:
|
| 564 |
+
await self.bot.db.execute(
|
| 565 |
+
"INSERT INTO guild_config(guild_id, free_games_channel_id, free_games_role_id) VALUES (?, ?, ?) "
|
| 566 |
+
"ON CONFLICT(guild_id) DO UPDATE SET free_games_channel_id = excluded.free_games_channel_id, free_games_role_id = excluded.free_games_role_id",
|
| 567 |
+
ctx.guild.id,
|
| 568 |
+
channel.id,
|
| 569 |
+
role.id if role else None,
|
| 570 |
+
)
|
| 571 |
+
mention = f" with role {role.mention}" if role else ""
|
| 572 |
+
await ctx.reply(f"🎁 Free games alerts channel set to {channel.mention}{mention}")
|
| 573 |
+
|
| 574 |
+
@setgroup.command(name="gamenews")
|
| 575 |
+
async def set_game_news(self, ctx: commands.Context, channel: discord.TextChannel, role: discord.Role | None = None) -> None:
|
| 576 |
+
await self.bot.db.execute(
|
| 577 |
+
"INSERT INTO guild_config(guild_id, game_news_channel_id, game_news_role_id) VALUES (?, ?, ?) "
|
| 578 |
+
"ON CONFLICT(guild_id) DO UPDATE SET game_news_channel_id = excluded.game_news_channel_id, game_news_role_id = excluded.game_news_role_id",
|
| 579 |
+
ctx.guild.id,
|
| 580 |
+
channel.id,
|
| 581 |
+
role.id if role else None,
|
| 582 |
+
)
|
| 583 |
+
mention = f" with role {role.mention}" if role else ""
|
| 584 |
+
await ctx.reply(f"📰 Game news alerts channel set to {channel.mention}{mention}")
|
| 585 |
+
|
| 586 |
+
@setgroup.command(name="supportai")
|
| 587 |
+
async def set_support_ai(self, ctx: commands.Context, channel: discord.TextChannel, enabled: bool = True) -> None:
|
| 588 |
+
await self.bot.db.execute(
|
| 589 |
+
"INSERT INTO guild_config(guild_id, support_channel_id, support_ai_enabled) VALUES (?, ?, ?) "
|
| 590 |
+
"ON CONFLICT(guild_id) DO UPDATE SET support_channel_id = excluded.support_channel_id, support_ai_enabled = excluded.support_ai_enabled",
|
| 591 |
+
ctx.guild.id,
|
| 592 |
+
channel.id,
|
| 593 |
+
1 if enabled else 0,
|
| 594 |
+
)
|
| 595 |
+
await ctx.reply(f"🛟 Support AI {'enabled' if enabled else 'disabled'} in {channel.mention}")
|
| 596 |
+
|
| 597 |
+
@setgroup.command(name="wisdom")
|
| 598 |
+
async def set_wisdom(self, ctx: commands.Context, channel: discord.TextChannel, enabled: bool = True) -> None:
|
| 599 |
+
await self.bot.db.execute(
|
| 600 |
+
"INSERT INTO guild_config(guild_id, wisdom_channel_id, wisdom_enabled) VALUES (?, ?, ?) "
|
| 601 |
+
"ON CONFLICT(guild_id) DO UPDATE SET wisdom_channel_id = excluded.wisdom_channel_id, wisdom_enabled = excluded.wisdom_enabled",
|
| 602 |
+
ctx.guild.id,
|
| 603 |
+
channel.id,
|
| 604 |
+
1 if enabled else 0,
|
| 605 |
+
)
|
| 606 |
+
await ctx.reply(f"🧠 Daily wisdom {'enabled' if enabled else 'disabled'} in {channel.mention}")
|
| 607 |
+
|
| 608 |
+
@setgroup.command(name="freegame")
|
| 609 |
+
async def set_free_game_alias(self, ctx: commands.Context, channel: discord.TextChannel, role: discord.Role | None = None) -> None:
|
| 610 |
+
await self.set_free_games_channel(ctx, channel, role)
|
| 611 |
+
|
| 612 |
+
@commands.hybrid_command(name="set_freegame", description=get_cmd_desc("commands.tools.set_freegame_desc"))
|
| 613 |
+
@commands.has_permissions(administrator=True)
|
| 614 |
+
async def set_freegame_panel(self, ctx: commands.Context, channel: discord.TextChannel, role: discord.Role | None = None) -> None:
|
| 615 |
+
await self.bot.db.execute(
|
| 616 |
+
"INSERT INTO guild_config(guild_id, free_games_channel_id, free_games_role_id) VALUES (?, ?, ?) "
|
| 617 |
+
"ON CONFLICT(guild_id) DO UPDATE SET free_games_channel_id=excluded.free_games_channel_id, free_games_role_id=excluded.free_games_role_id",
|
| 618 |
+
ctx.guild.id,
|
| 619 |
+
channel.id,
|
| 620 |
+
role.id if role else None,
|
| 621 |
+
)
|
| 622 |
+
view = FreeGameSetupView(self, ctx.guild.id, channel.id, role.id if role else None)
|
| 623 |
+
embed = discord.Embed(
|
| 624 |
+
title="<:animatedarrowgreen:1477261279428087979> Free Games Setup",
|
| 625 |
+
description=(
|
| 626 |
+
f"Channel: {channel.mention}\n"
|
| 627 |
+
f"Role ping: {role.mention if role else 'None'}\n"
|
| 628 |
+
"Use menu below to choose stores (Epic/Steam/GOG)."
|
| 629 |
+
),
|
| 630 |
+
color=discord.Color.green(),
|
| 631 |
+
)
|
| 632 |
+
await ctx.reply(embed=embed, view=view)
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
async def setup(bot: commands.Bot) -> None:
|
| 637 |
+
await bot.add_cog(Configuration(bot))
|
bot/cogs/developer.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import traceback
|
| 4 |
|
| 5 |
import discord
|
| 6 |
from discord.ext import commands
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
class Developer(commands.Cog):
|
| 10 |
def __init__(self, bot: commands.Bot) -> None:
|
|
@@ -13,43 +19,140 @@ class Developer(commands.Cog):
|
|
| 13 |
async def cog_check(self, ctx: commands.Context) -> bool:
|
| 14 |
return await self.bot.is_owner(ctx.author)
|
| 15 |
|
| 16 |
-
@commands.command(name="load", help="Load a cog extension by dotted path.")
|
| 17 |
-
async def load(self, ctx: commands.Context, extension: str) -> None:
|
| 18 |
await self.bot.load_extension(extension)
|
| 19 |
await ctx.reply(f"Loaded {extension}")
|
| 20 |
|
| 21 |
-
@commands.command(name="unload", help="Unload a cog extension by dotted path.")
|
| 22 |
-
async def unload(self, ctx: commands.Context, extension: str) -> None:
|
| 23 |
await self.bot.unload_extension(extension)
|
| 24 |
await ctx.reply(f"Unloaded {extension}")
|
| 25 |
|
| 26 |
-
@commands.command(name="reload", help="Reload a cog extension by dotted path.")
|
| 27 |
-
async def reload(self, ctx: commands.Context, extension: str) -> None:
|
| 28 |
await self.bot.reload_extension(extension)
|
| 29 |
await ctx.reply(f"Reloaded {extension}")
|
| 30 |
|
| 31 |
-
@commands.command(name="sync", help="Sync global slash commands to Discord.")
|
| 32 |
-
async def sync(self, ctx: commands.Context) -> None:
|
| 33 |
synced = await self.bot.tree.sync()
|
| 34 |
await ctx.reply(f"Synced {len(synced)} app commands")
|
| 35 |
|
| 36 |
-
@commands.command(name="shutdown", help="Gracefully shut down the bot process.")
|
| 37 |
-
async def shutdown(self, ctx: commands.Context) -> None:
|
| 38 |
await ctx.reply("Shutting down...")
|
| 39 |
await self.bot.close()
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
async def _safe_reply(self, ctx: commands.Context, message: str) -> None:
|
| 43 |
-
try:
|
| 44 |
-
await ctx.reply(message)
|
| 45 |
-
except (discord.NotFound, discord.InteractionResponded):
|
| 46 |
-
if ctx.channel:
|
| 47 |
-
await ctx.channel.send(message)
|
| 48 |
-
except discord.HTTPException as exc:
|
| 49 |
-
if exc.code not in {10062, 40060}:
|
| 50 |
-
raise
|
| 51 |
-
if ctx.channel:
|
| 52 |
-
await ctx.channel.send(message)
|
| 53 |
|
| 54 |
@commands.Cog.listener()
|
| 55 |
async def on_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import re
|
| 4 |
import traceback
|
| 5 |
|
| 6 |
import discord
|
| 7 |
from discord.ext import commands
|
| 8 |
|
| 9 |
+
from bot.emojis import (
|
| 10 |
+
CUSTOM_EMOJIS, _UI_ALIASES, FALLBACK_EMOJIS, _EMOJI_BOT,
|
| 11 |
+
_EMOJI_ID_RE, _normalize_key
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
|
| 15 |
class Developer(commands.Cog):
|
| 16 |
def __init__(self, bot: commands.Bot) -> None:
|
|
|
|
| 19 |
async def cog_check(self, ctx: commands.Context) -> bool:
|
| 20 |
return await self.bot.is_owner(ctx.author)
|
| 21 |
|
| 22 |
+
@commands.command(name="load", help="Load a cog extension by dotted path.")
|
| 23 |
+
async def load(self, ctx: commands.Context, extension: str) -> None:
|
| 24 |
await self.bot.load_extension(extension)
|
| 25 |
await ctx.reply(f"Loaded {extension}")
|
| 26 |
|
| 27 |
+
@commands.command(name="unload", help="Unload a cog extension by dotted path.")
|
| 28 |
+
async def unload(self, ctx: commands.Context, extension: str) -> None:
|
| 29 |
await self.bot.unload_extension(extension)
|
| 30 |
await ctx.reply(f"Unloaded {extension}")
|
| 31 |
|
| 32 |
+
@commands.command(name="reload", help="Reload a cog extension by dotted path.")
|
| 33 |
+
async def reload(self, ctx: commands.Context, extension: str) -> None:
|
| 34 |
await self.bot.reload_extension(extension)
|
| 35 |
await ctx.reply(f"Reloaded {extension}")
|
| 36 |
|
| 37 |
+
@commands.command(name="sync", help="Sync global slash commands to Discord.")
|
| 38 |
+
async def sync(self, ctx: commands.Context) -> None:
|
| 39 |
synced = await self.bot.tree.sync()
|
| 40 |
await ctx.reply(f"Synced {len(synced)} app commands")
|
| 41 |
|
| 42 |
+
@commands.command(name="shutdown", help="Gracefully shut down the bot process.")
|
| 43 |
+
async def shutdown(self, ctx: commands.Context) -> None:
|
| 44 |
await ctx.reply("Shutting down...")
|
| 45 |
await self.bot.close()
|
| 46 |
|
| 47 |
+
@commands.hybrid_command(name="emoji_scan", description="Show broken/unresolved custom emojis")
|
| 48 |
+
async def emoji_scan(self, ctx: commands.Context) -> None:
|
| 49 |
+
"""Scan all configured custom emojis and show which ones are broken."""
|
| 50 |
+
if not self.bot.is_ready() or not self.bot.user:
|
| 51 |
+
await ctx.reply("⏳ Bot is not ready yet.", ephemeral=True)
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
bot_emojis = {e.id: e for e in self.bot.emojis}
|
| 55 |
+
broken: list[tuple[str, str, str]] = []
|
| 56 |
+
working: list[tuple[str, str]] = []
|
| 57 |
+
total = 0
|
| 58 |
+
|
| 59 |
+
# Scan CUSTOM_EMOJIS from emojies.txt
|
| 60 |
+
for key, value in CUSTOM_EMOJIS.items():
|
| 61 |
+
total += 1
|
| 62 |
+
emoji_id = self._extract_id(value)
|
| 63 |
+
if emoji_id and emoji_id not in bot_emojis:
|
| 64 |
+
broken.append((key, value, "emoji not in bot cache"))
|
| 65 |
+
elif not emoji_id:
|
| 66 |
+
broken.append((key, value, "no valid emoji ID found"))
|
| 67 |
+
else:
|
| 68 |
+
working.append((key, value))
|
| 69 |
+
|
| 70 |
+
# Scan _UI_ALIASES — check if any alias resolves
|
| 71 |
+
checked_aliases: set[str] = set()
|
| 72 |
+
for ui_key, aliases in _UI_ALIASES.items():
|
| 73 |
+
for alias in aliases:
|
| 74 |
+
norm = _normalize_key(alias)
|
| 75 |
+
if norm in checked_aliases:
|
| 76 |
+
continue
|
| 77 |
+
checked_aliases.add(norm)
|
| 78 |
+
total += 1
|
| 79 |
+
alias_value = CUSTOM_EMOJIS.get(norm, "")
|
| 80 |
+
if not alias_value:
|
| 81 |
+
# Alias doesn't map to any configured emoji
|
| 82 |
+
broken.append((f"ui:{ui_key}→{alias}", alias_value or "(none)", "alias has no configured emoji"))
|
| 83 |
+
continue
|
| 84 |
+
aid = self._extract_id(alias_value)
|
| 85 |
+
if aid and aid not in bot_emojis:
|
| 86 |
+
broken.append((f"ui:{ui_key}→{alias}", alias_value, "aliased emoji not in cache"))
|
| 87 |
+
|
| 88 |
+
# Deduplicate broken list by key
|
| 89 |
+
seen_keys: set[str] = set()
|
| 90 |
+
unique_broken: list[tuple[str, str, str]] = []
|
| 91 |
+
for key, value, reason in broken:
|
| 92 |
+
if key not in seen_keys:
|
| 93 |
+
seen_keys.add(key)
|
| 94 |
+
unique_broken.append((key, value, reason))
|
| 95 |
+
broken = unique_broken
|
| 96 |
+
|
| 97 |
+
# Build embed
|
| 98 |
+
color = discord.Color.red() if broken else discord.Color.green()
|
| 99 |
+
embed = discord.Embed(
|
| 100 |
+
title="🔍 Bot Emoji Health Check",
|
| 101 |
+
description=(
|
| 102 |
+
f"**Total configured:** `{total}`\n"
|
| 103 |
+
f"**✅ Working:** `{len(working) + len(checked_aliases) - len(broken)}`\n"
|
| 104 |
+
f"**❌ Broken:** `{len(broken)}`\n"
|
| 105 |
+
f"**Bot cache size:** `{len(bot_emojis)}`"
|
| 106 |
+
),
|
| 107 |
+
color=color,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if broken:
|
| 111 |
+
# Group by reason
|
| 112 |
+
by_reason: dict[str, list[tuple[str, str]]] = {}
|
| 113 |
+
for key, value, reason in broken:
|
| 114 |
+
by_reason.setdefault(reason, []).append((key, value))
|
| 115 |
+
|
| 116 |
+
for reason, items in list(by_reason.items())[:10]: # Limit display
|
| 117 |
+
names = ", ".join(f"`{k}`" for k, v in items[:15])
|
| 118 |
+
if len(items) > 15:
|
| 119 |
+
names += f" (+{len(items) - 15} more)"
|
| 120 |
+
embed.add_field(
|
| 121 |
+
name=f"❌ {reason} ({len(items)})",
|
| 122 |
+
value=names[:1000],
|
| 123 |
+
inline=False,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
embed.set_footer(text="Run this after the bot joins new servers to refresh emoji cache")
|
| 127 |
+
|
| 128 |
+
if ctx.interaction:
|
| 129 |
+
await ctx.interaction.response.send_message(embed=embed, ephemeral=True)
|
| 130 |
+
else:
|
| 131 |
+
await ctx.reply(embed=embed, ephemeral=True)
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def _extract_id(value: str) -> int | None:
|
| 135 |
+
"""Extract numeric emoji ID from a custom emoji tag."""
|
| 136 |
+
if not value:
|
| 137 |
+
return None
|
| 138 |
+
m = _EMOJI_ID_RE.search(value)
|
| 139 |
+
if m:
|
| 140 |
+
return int(m.group(0))
|
| 141 |
+
if value.strip().isdigit():
|
| 142 |
+
return int(value.strip())
|
| 143 |
+
return None
|
| 144 |
|
| 145 |
+
async def _safe_reply(self, ctx: commands.Context, message: str) -> None:
|
| 146 |
+
try:
|
| 147 |
+
await ctx.reply(message)
|
| 148 |
+
except (discord.NotFound, discord.InteractionResponded):
|
| 149 |
+
if ctx.channel:
|
| 150 |
+
await ctx.channel.send(message)
|
| 151 |
+
except discord.HTTPException as exc:
|
| 152 |
+
if exc.code not in {10062, 40060}:
|
| 153 |
+
raise
|
| 154 |
+
if ctx.channel:
|
| 155 |
+
await ctx.channel.send(message)
|
| 156 |
|
| 157 |
@commands.Cog.listener()
|
| 158 |
async def on_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
bot/cogs/engagement.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bot/cogs/events.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bot/cogs/fun.py
CHANGED
|
@@ -1,980 +1,981 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Fun cog: Games, trivia, memes, and entertainment commands.
|
| 3 |
-
Enhanced with rich emoji decorations, beautiful formatting, and multi-language support.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import html
|
| 7 |
-
import random
|
| 8 |
-
|
| 9 |
-
import aiohttp
|
| 10 |
-
import discord
|
| 11 |
-
from discord.ext import commands
|
| 12 |
-
|
| 13 |
-
from bot.cogs.ai_suite import ImperialMotaz
|
| 14 |
-
from bot.theme import (
|
| 15 |
-
fancy_header, NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_ORANGE, NEON_BLUE,
|
| 16 |
-
panel_divider, gaming_embed, success_embed, error_embed, info_embed,
|
| 17 |
-
double_line, triple_line, shimmer, pick_neon_color
|
| 18 |
-
)
|
| 19 |
-
from bot.
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
{"q": "
|
| 31 |
-
{"q": "
|
| 32 |
-
{"q": "
|
| 33 |
-
{"q": "
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
{"q": "
|
| 38 |
-
{"q": "
|
| 39 |
-
{"q": "
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
{"q": "
|
| 44 |
-
{"q": "
|
| 45 |
-
{"q": "
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
{"q": "
|
| 52 |
-
{"q": "
|
| 53 |
-
{"q": "
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
{"q": "
|
| 58 |
-
{"q": "
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
{"q": "
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
self.
|
| 94 |
-
self.
|
| 95 |
-
self.
|
| 96 |
-
self.
|
| 97 |
-
self.
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
"
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
{"id": "
|
| 144 |
-
{"id": "
|
| 145 |
-
{"id": "
|
| 146 |
-
{"id": "
|
| 147 |
-
{"id": "
|
| 148 |
-
{"id": "
|
| 149 |
-
{"id": "
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
self.
|
| 157 |
-
self.
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
if
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
if
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
f"{
|
| 280 |
-
f"
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
f"{
|
| 287 |
-
f"
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
self.
|
| 358 |
-
self.
|
| 359 |
-
self.
|
| 360 |
-
for
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
btn.
|
| 379 |
-
btn.
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
self.
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
btn.
|
| 409 |
-
btn.
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
choices
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
"
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
f"
|
| 474 |
-
f"
|
| 475 |
-
f"{
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
"
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
f"
|
| 489 |
-
f"
|
| 490 |
-
f"{
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
"
|
| 554 |
-
"
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
embed.add_field(name=
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
"
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
embed.
|
| 617 |
-
embed.add_field(name="
|
| 618 |
-
embed.add_field(name="
|
| 619 |
-
embed.add_field(name="
|
| 620 |
-
embed.add_field(name="
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
if
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
f"
|
| 723 |
-
f"
|
| 724 |
-
f"
|
| 725 |
-
f"
|
| 726 |
-
f"
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
f"
|
| 734 |
-
f"
|
| 735 |
-
f"
|
| 736 |
-
f"
|
| 737 |
-
f"
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
f"
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
f"
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
f"
|
| 816 |
-
f"
|
| 817 |
-
f"{
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
f"
|
| 827 |
-
f"
|
| 828 |
-
f"{
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
f"
|
| 848 |
-
f"
|
| 849 |
-
f"{
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
f"
|
| 860 |
-
f"
|
| 861 |
-
f"{
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
guild_id,
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
"
|
| 972 |
-
guild_id,
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
amount,
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fun cog: Games, trivia, memes, and entertainment commands.
|
| 3 |
+
Enhanced with rich emoji decorations, beautiful formatting, and multi-language support.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import html
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
import aiohttp
|
| 10 |
+
import discord
|
| 11 |
+
from discord.ext import commands
|
| 12 |
+
|
| 13 |
+
from bot.cogs.ai_suite import ImperialMotaz
|
| 14 |
+
from bot.theme import (
|
| 15 |
+
fancy_header, NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_ORANGE, NEON_BLUE,
|
| 16 |
+
panel_divider, gaming_embed, success_embed, error_embed, info_embed,
|
| 17 |
+
double_line, triple_line, shimmer, pick_neon_color
|
| 18 |
+
)
|
| 19 |
+
from bot.i18n import get_cmd_desc
|
| 20 |
+
from bot.emojis import (
|
| 21 |
+
ui, E_GAMEHUB, E_ARROW_PINK, E_ARROW_GREEN, E_ARROW_PURPLE, E_STAR,
|
| 22 |
+
E_TROPHY, E_FIRE, E_SPARKLE, E_DIZZY, E_BOOM, E_ZAP, E_DIAMOND, E_CROWN, E_GEM
|
| 23 |
+
)
|
| 24 |
+
from bot.utils.shared import fetch_gaming_news, fetch_free_games, store_icon, FreeGameClaimView
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
TRIVIA_BANK: dict[str, dict[str, list[dict[str, object]]]] = {
|
| 28 |
+
"en": {
|
| 29 |
+
"gaming": [
|
| 30 |
+
{"q": "Which studio created The Witcher 3?", "c": ["CD Projekt Red", "Rockstar", "Ubisoft", "Bethesda"], "a": 0},
|
| 31 |
+
{"q": "What is the default block in Minecraft?", "c": ["Stone", "Dirt", "Grass Block", "Sand"], "a": 2},
|
| 32 |
+
{"q": "In which game would you find Master Chief?", "c": ["Halo", "Destiny", "Call of Duty", "Gears of War"], "a": 0},
|
| 33 |
+
{"q": "What year was Fortnite released?", "c": ["2015", "2017", "2018", "2019"], "a": 1},
|
| 34 |
+
{"q": "Which company makes the PlayStation?", "c": ["Microsoft", "Nintendo", "Sony", "Sega"], "a": 2},
|
| 35 |
+
],
|
| 36 |
+
"movies": [
|
| 37 |
+
{"q": "Who directed Inception?", "c": ["Christopher Nolan", "James Cameron", "Denis Villeneuve", "Ridley Scott"], "a": 0},
|
| 38 |
+
{"q": "In the MCU, who is Peter Parker?", "c": ["Iron Man", "Spider-Man", "Doctor Strange", "Hawkeye"], "a": 1},
|
| 39 |
+
{"q": "What is the highest-grossing film of all time?", "c": ["Avengers: Endgame", "Avatar", "Titanic", "Star Wars"], "a": 1},
|
| 40 |
+
{"q": "Who played the Joker in The Dark Knight?", "c": ["Joaquin Phoenix", "Jack Nicholson", "Heath Ledger", "Jared Leto"], "a": 2},
|
| 41 |
+
],
|
| 42 |
+
"series": [
|
| 43 |
+
{"q": "Which series features Walter White?", "c": ["Narcos", "Breaking Bad", "Dark", "Sherlock"], "a": 1},
|
| 44 |
+
{"q": "In Game of Thrones, what is Jon Snow's house at first?", "c": ["Lannister", "Baratheon", "Stark", "Targaryen"], "a": 2},
|
| 45 |
+
{"q": "What is the name of the coffee shop in Friends?", "c": ["Central Park", "Central Perk", "Coffee House", "The Brew"], "a": 1},
|
| 46 |
+
{"q": "How many seasons does Stranger Things have?", "c": ["3", "4", "5", "6"], "a": 2},
|
| 47 |
+
],
|
| 48 |
+
},
|
| 49 |
+
"ar": {
|
| 50 |
+
"gaming": [
|
| 51 |
+
{"q": "أي شركة طورت لعبة The Witcher 3؟", "c": ["CD Projekt Red", "Rockstar", "Ubisoft", "Bethesda"], "a": 0},
|
| 52 |
+
{"q": "ما هو البلوك الافتراضي في Minecraft؟", "c": ["Stone", "Dirt", "Grass Block", "Sand"], "a": 2},
|
| 53 |
+
{"q": "في أي لعبة تجد Master Chief؟", "c": ["Halo", "Destiny", "Call of Duty", "Gears of War"], "a": 0},
|
| 54 |
+
{"q": "متى صدرت Fortnite؟", "c": ["2015", "2017", "2018", "2019"], "a": 1},
|
| 55 |
+
],
|
| 56 |
+
"movies": [
|
| 57 |
+
{"q": "من مخرج فيلم Inception؟", "c": ["كريستوفر نولان", "جيمس كاميرون", "دينيس فيلنوف", "ريدلي سكوت"], "a": 0},
|
| 58 |
+
{"q": "في عالم مارفل، بيتر باركر هو؟", "c": ["Iron Man", "Spider-Man", "Doctor Strange", "Hawkeye"], "a": 1},
|
| 59 |
+
{"q": "ما هو أعلى فيلم إيرادات في التاريخ؟", "c": ["Avengers: Endgame", "Avatar", "Titanic", "Star Wars"], "a": 1},
|
| 60 |
+
],
|
| 61 |
+
"series": [
|
| 62 |
+
{"q": "أي مسلسل يظهر فيه Walter White؟", "c": ["Narcos", "Breaking Bad", "Dark", "Sherlock"], "a": 1},
|
| 63 |
+
{"q": "في Game of Thrones، جون سنو كان ضمن أي بيت بالبداية؟", "c": ["Lannister", "Baratheon", "Stark", "Targaryen"], "a": 2},
|
| 64 |
+
],
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class TriviaAnswerButton(discord.ui.Button["TriviaView"]):
|
| 70 |
+
def __init__(self, label: str, index: int) -> None:
|
| 71 |
+
super().__init__(label=label[:80], style=discord.ButtonStyle.secondary)
|
| 72 |
+
self.index = index
|
| 73 |
+
|
| 74 |
+
async def callback(self, interaction: discord.Interaction) -> None:
|
| 75 |
+
await self.view.on_answer(interaction, self.index)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class FreeGameClaimView(discord.ui.View):
|
| 79 |
+
def __init__(self, url: str) -> None:
|
| 80 |
+
super().__init__(timeout=None)
|
| 81 |
+
self.add_item(
|
| 82 |
+
discord.ui.Button(
|
| 83 |
+
label=f"{ui('gift')} Claim Your Prize {ui('gift')}",
|
| 84 |
+
style=discord.ButtonStyle.link,
|
| 85 |
+
url=url,
|
| 86 |
+
)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class TriviaView(discord.ui.View):
|
| 91 |
+
def __init__(self, owner_id: int, correct_index: int, lang: str, cog: "Fun", guild_id: int | None) -> None:
|
| 92 |
+
super().__init__(timeout=None)
|
| 93 |
+
self.owner_id = owner_id
|
| 94 |
+
self.correct_index = correct_index
|
| 95 |
+
self.lang = lang
|
| 96 |
+
self.answered = False
|
| 97 |
+
self.cog = cog
|
| 98 |
+
self.guild_id = guild_id
|
| 99 |
+
|
| 100 |
+
async def on_answer(self, interaction: discord.Interaction, picked_index: int) -> None:
|
| 101 |
+
if interaction.user.id != self.owner_id:
|
| 102 |
+
msg = "❌ هذا السؤال لصاحب الأمر فقط." if self.lang == "ar" else "❌ This question is for the command author only."
|
| 103 |
+
await interaction.response.send_message(msg, ephemeral=True)
|
| 104 |
+
return
|
| 105 |
+
if self.answered:
|
| 106 |
+
msg = "⚠️ تمت الإجابة بالفعل." if self.lang == "ar" else "⚠️ Already answered."
|
| 107 |
+
await interaction.response.send_message(msg, ephemeral=True)
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
self.answered = True
|
| 111 |
+
is_correct = picked_index == self.correct_index
|
| 112 |
+
for idx, child in enumerate(self.children):
|
| 113 |
+
if isinstance(child, discord.ui.Button):
|
| 114 |
+
child.disabled = True
|
| 115 |
+
if idx == self.correct_index:
|
| 116 |
+
child.style = discord.ButtonStyle.success
|
| 117 |
+
elif idx == picked_index and not is_correct:
|
| 118 |
+
child.style = discord.ButtonStyle.danger
|
| 119 |
+
|
| 120 |
+
if is_correct:
|
| 121 |
+
msg = f"✅ إجابة صحيحة! {E_STAR}" if self.lang == "ar" else f"✅ Correct answer! {E_STAR}"
|
| 122 |
+
else:
|
| 123 |
+
msg = "❌ إجابة خاطئة." if self.lang == "ar" else "❌ Wrong answer."
|
| 124 |
+
|
| 125 |
+
await interaction.response.edit_message(view=self)
|
| 126 |
+
if is_correct and interaction.guild and self.guild_id:
|
| 127 |
+
reward = random.randint(20, 45)
|
| 128 |
+
await self.cog.bot.db.execute(
|
| 129 |
+
"INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) "
|
| 130 |
+
"ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = wallet + excluded.wallet",
|
| 131 |
+
self.guild_id,
|
| 132 |
+
interaction.user.id,
|
| 133 |
+
reward,
|
| 134 |
+
)
|
| 135 |
+
if self.lang == "ar":
|
| 136 |
+
msg += f" 💰 **+{reward}** عملة!"
|
| 137 |
+
else:
|
| 138 |
+
msg += f" 💰 **+{reward}** coins!"
|
| 139 |
+
await interaction.followup.send(msg, ephemeral=True)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
GAME_HUB_GAMES = [
|
| 143 |
+
{"id": "chess", "name": "Chess", "emoji": "♟️", "description": "Strategic board game"},
|
| 144 |
+
{"id": "checkers", "name": "Checkers", "emoji": "🔴", "description": "Classic draughts game"},
|
| 145 |
+
{"id": "connect4", "name": "Connect 4", "emoji": "🔴", "description": "Drop and connect"},
|
| 146 |
+
{"id": "othello", "name": "Othello", "emoji": "⚪", "description": "Reversi strategy"},
|
| 147 |
+
{"id": "tictactoe", "name": "TicTacToe", "emoji": "❎", "description": "X and O battle"},
|
| 148 |
+
{"id": "rps", "name": "Rock Paper Scissors", "emoji": "✂️", "description": "Quick decision game"},
|
| 149 |
+
{"id": "trivia", "name": "Trivia Challenge", "emoji": "🧠", "description": "Knowledge contest"},
|
| 150 |
+
{"id": "guess", "name": "Number Guess", "emoji": "🔢", "description": "Guess 1-10"},
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class GameHubSelect(discord.ui.Select):
|
| 155 |
+
def __init__(self, cog: "Fun", invitee_id: int | None, lang: str = "en") -> None:
|
| 156 |
+
self.cog = cog
|
| 157 |
+
self.invitee_id = invitee_id
|
| 158 |
+
self.lang = lang
|
| 159 |
+
|
| 160 |
+
options = []
|
| 161 |
+
for game in GAME_HUB_GAMES:
|
| 162 |
+
if lang == "ar":
|
| 163 |
+
desc = game["description"]
|
| 164 |
+
else:
|
| 165 |
+
desc = game["description"]
|
| 166 |
+
options.append(
|
| 167 |
+
discord.SelectOption(
|
| 168 |
+
label=game["name"],
|
| 169 |
+
value=game["id"],
|
| 170 |
+
description=desc,
|
| 171 |
+
emoji=game["emoji"]
|
| 172 |
+
)
|
| 173 |
+
)
|
| 174 |
+
super().__init__(
|
| 175 |
+
placeholder="🎮 Choose a game",
|
| 176 |
+
min_values=1,
|
| 177 |
+
max_values=1,
|
| 178 |
+
options=options,
|
| 179 |
+
custom_id="gamehub_select",
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
async def callback(self, interaction: discord.Interaction) -> None:
|
| 183 |
+
if not interaction.guild:
|
| 184 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 185 |
+
return
|
| 186 |
+
|
| 187 |
+
guild = interaction.guild
|
| 188 |
+
game_id = self.values[0]
|
| 189 |
+
invitee = guild.get_member(self.invitee_id) if self.invitee_id else None
|
| 190 |
+
guild_id = guild.id
|
| 191 |
+
lang = await self.cog.bot.get_guild_language(guild_id)
|
| 192 |
+
|
| 193 |
+
category = discord.utils.get(guild.categories, name="🕹️ Game Rooms")
|
| 194 |
+
if category is None:
|
| 195 |
+
category = await guild.create_category("🕹️ Game Rooms", reason="GameHub temporary rooms")
|
| 196 |
+
|
| 197 |
+
overwrites = {
|
| 198 |
+
guild.default_role: discord.PermissionOverwrite(view_channel=False),
|
| 199 |
+
interaction.user: discord.PermissionOverwrite(view_channel=True, send_messages=True, read_message_history=True),
|
| 200 |
+
}
|
| 201 |
+
if invitee:
|
| 202 |
+
overwrites[invitee] = discord.PermissionOverwrite(view_channel=True, send_messages=True, read_message_history=True)
|
| 203 |
+
if guild.me:
|
| 204 |
+
overwrites[guild.me] = discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True)
|
| 205 |
+
|
| 206 |
+
room = await guild.create_text_channel(
|
| 207 |
+
name=f"game-{game_id}-{interaction.user.name[:8].lower()}",
|
| 208 |
+
category=category,
|
| 209 |
+
overwrites=overwrites,
|
| 210 |
+
reason=f"GameHub room for {interaction.user}",
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
board_cog = self.cog.bot.get_cog("BoardGames")
|
| 214 |
+
try:
|
| 215 |
+
if game_id == "tictactoe":
|
| 216 |
+
view = TicTacToeView(interaction.user, invitee)
|
| 217 |
+
first = interaction.user.mention
|
| 218 |
+
second = invitee.mention if invitee else "🤖 Bot"
|
| 219 |
+
if lang == "ar":
|
| 220 |
+
msg = await room.send(f"❎ تيك تاك تو: {first} (X) ضد {second} (O)\n🎮 الدور: {interaction.user.mention}", view=view)
|
| 221 |
+
else:
|
| 222 |
+
msg = await room.send(f"❎ TicTacToe: {first} (X) vs {second} (O)\n🎮 Turn: {interaction.user.mention}", view=view)
|
| 223 |
+
if invitee is None:
|
| 224 |
+
await view._maybe_bot(msg)
|
| 225 |
+
elif game_id == "trivia":
|
| 226 |
+
# Start trivia game
|
| 227 |
+
item = random.choice(TRIVIA_BANK.get(lang, TRIVIA_BANK["en"])["gaming"])
|
| 228 |
+
view = TriviaView(owner_id=interaction.user.id, correct_index=int(item["a"]), lang=lang, cog=self.cog, guild_id=guild_id)
|
| 229 |
+
for idx, choice in enumerate(item["c"]):
|
| 230 |
+
view.add_item(TriviaAnswerButton(str(choice), idx))
|
| 231 |
+
|
| 232 |
+
if lang == "ar":
|
| 233 |
+
q_title = "🧠 سؤال تراڤيا!"
|
| 234 |
+
else:
|
| 235 |
+
q_title = "🧠 Trivia Time!"
|
| 236 |
+
|
| 237 |
+
embed = discord.Embed(
|
| 238 |
+
title=q_title,
|
| 239 |
+
description=f"{panel_divider('blue')}\n❓ {str(item['q'])}\n{panel_divider('blue')}",
|
| 240 |
+
color=NEON_CYAN,
|
| 241 |
+
)
|
| 242 |
+
await room.send(embed=embed, view=view)
|
| 243 |
+
elif game_id == "rps":
|
| 244 |
+
# RPS game in channel
|
| 245 |
+
if lang == "ar":
|
| 246 |
+
await room.send(f"✂️ حجر ورقة مقص!\nاستخدم `rps rock/paper/scissors` للعب!")
|
| 247 |
+
else:
|
| 248 |
+
await room.send(f"✂️ Rock Paper Scissors!\nUse `rps rock/paper/scissors` to play!")
|
| 249 |
+
elif game_id == "guess":
|
| 250 |
+
# Number guessing game
|
| 251 |
+
secret = random.randint(1, 10)
|
| 252 |
+
if lang == "ar":
|
| 253 |
+
await room.send(f"🔢 خمن الرقم من 1 إلى 10!\nاستخدم `/guess <رقم>` للعب!")
|
| 254 |
+
else:
|
| 255 |
+
await room.send(f"🔢 Guess a number from 1 to 10!\nUse `/guess <number>` to play!")
|
| 256 |
+
elif board_cog and game_id in {"chess", "checkers", "connect4", "othello"}:
|
| 257 |
+
fake_ctx = type("_Ctx", (), {"guild": guild, "channel": room, "author": interaction.user, "reply": room.send})
|
| 258 |
+
opponent = invitee
|
| 259 |
+
ok = await board_cog.start_game_session(fake_ctx, game_id, opponent)
|
| 260 |
+
if not ok:
|
| 261 |
+
if lang == "ar":
|
| 262 |
+
await room.send("⚠️ تعذر تشغيل اللعبة. اختر لعبة أخرى.")
|
| 263 |
+
else:
|
| 264 |
+
await room.send("⚠️ Could not initialize selected board game. Choose another one.")
|
| 265 |
+
else:
|
| 266 |
+
game_name = next((g["name"] for g in GAME_HUB_GAMES if g["id"] == game_id), game_id.title())
|
| 267 |
+
if lang == "ar":
|
| 268 |
+
await room.send(f"🎮 **{game_name}** الغرفة جاهزة. العب الآن! {E_FIRE}")
|
| 269 |
+
else:
|
| 270 |
+
await room.send(f"🎮 **{game_name}** room is ready. Play now! {E_FIRE}")
|
| 271 |
+
except Exception:
|
| 272 |
+
if lang == "ar":
|
| 273 |
+
await room.send("⚠️ تعذر تشغيل هذه اللعبة. جرب إنشاء غرفة أخرى.")
|
| 274 |
+
else:
|
| 275 |
+
await room.send("⚠️ Could not start this game. Try creating another room.")
|
| 276 |
+
|
| 277 |
+
if lang == "ar":
|
| 278 |
+
invite_text = (
|
| 279 |
+
f"🎮 **{game_id.title()}** الغرفة جاهزة!\n"
|
| 280 |
+
f"{panel_divider('purple')}\n"
|
| 281 |
+
f"👤 **المضيف:** {interaction.user.mention}"
|
| 282 |
+
)
|
| 283 |
+
close_text = "🔒 أغلق الغرفة عند الانتهاء 👇"
|
| 284 |
+
else:
|
| 285 |
+
invite_text = (
|
| 286 |
+
f"🎮 **{game_id.title()}** Room Ready!\n"
|
| 287 |
+
f"{panel_divider('purple')}\n"
|
| 288 |
+
f"👤 **Host:** {interaction.user.mention}"
|
| 289 |
+
)
|
| 290 |
+
close_text = "🔒 Close the room when done 👇"
|
| 291 |
+
|
| 292 |
+
if invitee:
|
| 293 |
+
if lang == "ar":
|
| 294 |
+
invite_text += f"\n🎯 **مدعو:** {invitee.mention}"
|
| 295 |
+
else:
|
| 296 |
+
invite_text += f"\n🎯 **Invited:** {invitee.mention}"
|
| 297 |
+
invite_text += f"\n\n{close_text}"
|
| 298 |
+
await room.send(invite_text, view=RoomControlView(interaction.user.id))
|
| 299 |
+
|
| 300 |
+
if lang == "ar":
|
| 301 |
+
response_msg = f"✅ تم إنشاء الغرفة: {room.mention} 🎮"
|
| 302 |
+
if invitee:
|
| 303 |
+
response_msg += f" | تمت دعوة {invitee.mention}"
|
| 304 |
+
else:
|
| 305 |
+
response_msg = f"✅ Room created: {room.mention} 🎮"
|
| 306 |
+
if invitee:
|
| 307 |
+
response_msg += f" | Invited {invitee.mention}"
|
| 308 |
+
await interaction.response.send_message(response_msg, ephemeral=True)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
class GameHubView(discord.ui.View):
|
| 312 |
+
def __init__(self, cog: "Fun", invitee_id: int | None = None, lang: str = "en") -> None:
|
| 313 |
+
super().__init__(timeout=None)
|
| 314 |
+
self.add_item(GameHubSelect(cog, invitee_id, lang))
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
class RoomControlView(discord.ui.View):
|
| 318 |
+
def __init__(self, owner_id: int) -> None:
|
| 319 |
+
super().__init__(timeout=None)
|
| 320 |
+
self.owner_id = owner_id
|
| 321 |
+
|
| 322 |
+
@discord.ui.button(label="End Game & Close Room", style=discord.ButtonStyle.danger, emoji=ui("lock"), custom_id="room_close")
|
| 323 |
+
async def close_room(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 324 |
+
if not isinstance(interaction.channel, discord.TextChannel):
|
| 325 |
+
await interaction.response.send_message("Text channel only.", ephemeral=True)
|
| 326 |
+
return
|
| 327 |
+
if interaction.user.id != self.owner_id and not interaction.user.guild_permissions.manage_channels:
|
| 328 |
+
await interaction.response.send_message("❌ Only room owner or admins can close this room.", ephemeral=True)
|
| 329 |
+
return
|
| 330 |
+
await interaction.response.send_message("🔒 Closing room...", ephemeral=True)
|
| 331 |
+
try:
|
| 332 |
+
await interaction.channel.delete(reason=f"Game room closed by {interaction.user}")
|
| 333 |
+
except Exception:
|
| 334 |
+
pass
|
| 335 |
+
|
| 336 |
+
@discord.ui.button(label="Invite Friend", style=discord.ButtonStyle.success, emoji=ui("plus"), custom_id="room_invite")
|
| 337 |
+
async def invite_friend(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 338 |
+
if interaction.user.id != self.owner_id:
|
| 339 |
+
await interaction.response.send_message("❌ Only room owner can invite.", ephemeral=True)
|
| 340 |
+
return
|
| 341 |
+
await interaction.response.send_message("➕ Use `/gamehub @user` to invite someone to a new game room!", ephemeral=True)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class TicTacToeButton(discord.ui.Button["TicTacToeView"]):
|
| 345 |
+
def __init__(self, index: int) -> None:
|
| 346 |
+
super().__init__(label=" ", style=discord.ButtonStyle.secondary, row=index // 3)
|
| 347 |
+
self.index = index
|
| 348 |
+
|
| 349 |
+
async def callback(self, interaction: discord.Interaction) -> None:
|
| 350 |
+
if self.view:
|
| 351 |
+
await self.view.play(interaction, self.index)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class TicTacToeView(discord.ui.View):
|
| 355 |
+
def __init__(self, player_x: discord.Member, player_o: discord.Member | None) -> None:
|
| 356 |
+
super().__init__(timeout=None)
|
| 357 |
+
self.player_x = player_x
|
| 358 |
+
self.player_o = player_o
|
| 359 |
+
self.turn_x = True
|
| 360 |
+
self.board = ["" for _ in range(9)]
|
| 361 |
+
for i in range(9):
|
| 362 |
+
self.add_item(TicTacToeButton(i))
|
| 363 |
+
|
| 364 |
+
def _check(self, mark: str) -> bool:
|
| 365 |
+
lines = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]
|
| 366 |
+
return any(self.board[a]==self.board[b]==self.board[c]==mark for a,b,c in lines)
|
| 367 |
+
|
| 368 |
+
def _free(self) -> list[int]:
|
| 369 |
+
return [i for i,v in enumerate(self.board) if not v]
|
| 370 |
+
|
| 371 |
+
async def _maybe_bot(self, message: discord.Message) -> None:
|
| 372 |
+
if self.player_o is not None or not self._free() or not self.turn_x:
|
| 373 |
+
return
|
| 374 |
+
move = random.choice(self._free())
|
| 375 |
+
self.board[move] = "O"
|
| 376 |
+
btn = self.children[move]
|
| 377 |
+
if isinstance(btn, discord.ui.Button):
|
| 378 |
+
btn.label = "O"
|
| 379 |
+
btn.style = discord.ButtonStyle.danger
|
| 380 |
+
btn.disabled = True
|
| 381 |
+
self.turn_x = True
|
| 382 |
+
if self._check("O"):
|
| 383 |
+
for child in self.children:
|
| 384 |
+
if isinstance(child, discord.ui.Button):
|
| 385 |
+
child.disabled = True
|
| 386 |
+
await message.edit(content=f"🤖 Bot won TicTacToe! {E_BOOM}", view=self)
|
| 387 |
+
return
|
| 388 |
+
if not self._free():
|
| 389 |
+
await message.edit(content="🤝 TicTacToe draw!", view=self)
|
| 390 |
+
return
|
| 391 |
+
await message.edit(content=f"❎ Turn: {self.player_x.mention}", view=self)
|
| 392 |
+
|
| 393 |
+
async def play(self, interaction: discord.Interaction, index: int) -> None:
|
| 394 |
+
if self.board[index]:
|
| 395 |
+
await interaction.response.send_message("This cell is already used.", ephemeral=True)
|
| 396 |
+
return
|
| 397 |
+
current = self.player_x if self.turn_x else self.player_o
|
| 398 |
+
if self.turn_x and interaction.user.id != self.player_x.id:
|
| 399 |
+
await interaction.response.send_message("It is X player's turn.", ephemeral=True)
|
| 400 |
+
return
|
| 401 |
+
if not self.turn_x and self.player_o and interaction.user.id != self.player_o.id:
|
| 402 |
+
await interaction.response.send_message("It is O player's turn.", ephemeral=True)
|
| 403 |
+
return
|
| 404 |
+
mark = "X" if self.turn_x else "O"
|
| 405 |
+
self.board[index] = mark
|
| 406 |
+
btn = self.children[index]
|
| 407 |
+
if isinstance(btn, discord.ui.Button):
|
| 408 |
+
btn.label = mark
|
| 409 |
+
btn.style = discord.ButtonStyle.success if mark == "X" else discord.ButtonStyle.danger
|
| 410 |
+
btn.disabled = True
|
| 411 |
+
|
| 412 |
+
if self._check(mark):
|
| 413 |
+
for child in self.children:
|
| 414 |
+
if isinstance(child, discord.ui.Button):
|
| 415 |
+
child.disabled = True
|
| 416 |
+
winner = interaction.user.mention if self.player_o else (self.player_x.mention if mark == "X" else "🤖 Bot")
|
| 417 |
+
await interaction.response.edit_message(content=f"🏆 TicTacToe Winner: {winner}! {E_TROPHY}", view=self)
|
| 418 |
+
return
|
| 419 |
+
|
| 420 |
+
if not self._free():
|
| 421 |
+
await interaction.response.edit_message(content="🤝 TicTacToe draw!", view=self)
|
| 422 |
+
return
|
| 423 |
+
|
| 424 |
+
self.turn_x = not self.turn_x
|
| 425 |
+
nxt = self.player_x.mention if self.turn_x else (self.player_o.mention if self.player_o else "🤖 Bot")
|
| 426 |
+
await interaction.response.edit_message(content=f"🎮 Turn: {nxt}", view=self)
|
| 427 |
+
if not self.turn_x and self.player_o is None and interaction.message:
|
| 428 |
+
await self._maybe_bot(interaction.message)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
class Fun(commands.Cog):
|
| 432 |
+
"""Games and entertainment with beautiful panels and multi-language support."""
|
| 433 |
+
|
| 434 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 435 |
+
self.bot = bot
|
| 436 |
+
|
| 437 |
+
async def cog_load(self) -> None:
|
| 438 |
+
self.bot.add_view(GameHubView(self))
|
| 439 |
+
|
| 440 |
+
async def _fetch_trivia_api(self) -> dict[str, object] | None:
|
| 441 |
+
url = "https://opentdb.com/api.php?amount=50"
|
| 442 |
+
async with aiohttp.ClientSession() as session:
|
| 443 |
+
async with session.get(url, timeout=15) as response:
|
| 444 |
+
data = await response.json(content_type=None)
|
| 445 |
+
results = data.get("results") if isinstance(data, dict) else None
|
| 446 |
+
if not isinstance(results, list) or not results:
|
| 447 |
+
return None
|
| 448 |
+
item = random.choice(results)
|
| 449 |
+
question = html.unescape(str(item.get("question", "")).strip())
|
| 450 |
+
correct = html.unescape(str(item.get("correct_answer", "")).strip())
|
| 451 |
+
wrong = [html.unescape(str(c).strip()) for c in item.get("incorrect_answers", []) if str(c).strip()]
|
| 452 |
+
if not question or not correct or not wrong:
|
| 453 |
+
return None
|
| 454 |
+
choices = wrong[:]
|
| 455 |
+
choices.append(correct)
|
| 456 |
+
random.shuffle(choices)
|
| 457 |
+
answer_index = choices.index(correct)
|
| 458 |
+
return {"q": question, "c": choices[:4], "a": answer_index}
|
| 459 |
+
|
| 460 |
+
@commands.hybrid_command(name="8ball", description=get_cmd_desc("commands.tools.8ball_desc"))
|
| 461 |
+
async def eightball(self, ctx: commands.Context, *, question: str) -> None:
|
| 462 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 463 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 464 |
+
|
| 465 |
+
if lang == "ar":
|
| 466 |
+
answers = [
|
| 467 |
+
"نعم! ✅", "لا ❌", "ربما 🤔", "بالتأكيد! 💯",
|
| 468 |
+
"ليس الآن ⏰", "اسأل لاحقاً 🔮", "100% نعم! 🔥", "غير واضح 🌫️"
|
| 469 |
+
]
|
| 470 |
+
embed = discord.Embed(
|
| 471 |
+
title="🎱 الكرة السحرية",
|
| 472 |
+
description=(
|
| 473 |
+
f"{panel_divider('pink')}\n"
|
| 474 |
+
f"❓ **السؤال:** {question}\n"
|
| 475 |
+
f"🎱 **الجواب:** {random.choice(answers)}\n"
|
| 476 |
+
f"{panel_divider('pink')}"
|
| 477 |
+
),
|
| 478 |
+
color=NEON_PINK,
|
| 479 |
+
)
|
| 480 |
+
else:
|
| 481 |
+
answers = [
|
| 482 |
+
"Yes! ✅", "No ❌", "Maybe 🤔", "Definitely! 💯",
|
| 483 |
+
"Not now ⏰", "Ask later 🔮", "100% Yes! 🔥", "Unclear 🌫️"
|
| 484 |
+
]
|
| 485 |
+
embed = discord.Embed(
|
| 486 |
+
title="🎱 Magic 8 Ball",
|
| 487 |
+
description=(
|
| 488 |
+
f"{panel_divider('pink')}\n"
|
| 489 |
+
f"❓ **Question:** {question}\n"
|
| 490 |
+
f"🎱 **Answer:** {random.choice(answers)}\n"
|
| 491 |
+
f"{panel_divider('pink')}"
|
| 492 |
+
),
|
| 493 |
+
color=NEON_PINK,
|
| 494 |
+
)
|
| 495 |
+
await ctx.reply(embed=embed)
|
| 496 |
+
|
| 497 |
+
@commands.hybrid_command(name="meme", description=get_cmd_desc("commands.tools.meme_desc"))
|
| 498 |
+
async def meme(self, ctx: commands.Context) -> None:
|
| 499 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 500 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 501 |
+
|
| 502 |
+
url = "https://meme-api.com/gimme"
|
| 503 |
+
try:
|
| 504 |
+
async with aiohttp.ClientSession() as session:
|
| 505 |
+
async with session.get(url, timeout=10) as response:
|
| 506 |
+
data = await response.json()
|
| 507 |
+
except Exception:
|
| 508 |
+
if lang == "ar":
|
| 509 |
+
await ctx.reply("⚠️ تعذر تحميل الميم الآن. جرب مرة أخرى! 🔄")
|
| 510 |
+
else:
|
| 511 |
+
await ctx.reply("⚠️ Could not load meme right now. Try again in a minute! 🔄")
|
| 512 |
+
return
|
| 513 |
+
|
| 514 |
+
embed = discord.Embed(
|
| 515 |
+
title=f"😂 {data.get('title', 'Random Meme')}",
|
| 516 |
+
color=NEON_CYAN,
|
| 517 |
+
url=data.get("postLink")
|
| 518 |
+
)
|
| 519 |
+
if data.get("url"):
|
| 520 |
+
embed.set_image(url=data["url"])
|
| 521 |
+
embed.set_footer(text=f"👍 {data.get('ups', 0)} | r/{data.get('subreddit', 'meme')}")
|
| 522 |
+
await ctx.reply(embed=embed)
|
| 523 |
+
|
| 524 |
+
@commands.hybrid_command(name="trivia", description=get_cmd_desc("commands.tools.trivia_desc"), hidden=True, with_app_command=False)
|
| 525 |
+
async def trivia(self, ctx: commands.Context, category: str = "", source: str = "local") -> None:
|
| 526 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 527 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 528 |
+
|
| 529 |
+
normalized_source = source.strip().lower() or "local"
|
| 530 |
+
if normalized_source not in {"local", "api"}:
|
| 531 |
+
if lang == "ar":
|
| 532 |
+
await ctx.reply("❌ المصدر يجب أن يكون `local` أو `api`")
|
| 533 |
+
else:
|
| 534 |
+
await ctx.reply("❌ Source must be `local` or `api`")
|
| 535 |
+
return
|
| 536 |
+
|
| 537 |
+
normalized = category.strip().lower()
|
| 538 |
+
item: dict[str, object] | None = None
|
| 539 |
+
if lang == "ar":
|
| 540 |
+
topic_text = "الألعاب • الأفلام • المسلسلات"
|
| 541 |
+
else:
|
| 542 |
+
topic_text = "Gaming • Movies • Series"
|
| 543 |
+
|
| 544 |
+
if normalized_source == "api":
|
| 545 |
+
try:
|
| 546 |
+
item = await self._fetch_trivia_api()
|
| 547 |
+
topic_text = "OpenTDB (50 questions pool)"
|
| 548 |
+
except Exception:
|
| 549 |
+
item = None
|
| 550 |
+
|
| 551 |
+
if item is None:
|
| 552 |
+
aliases = {
|
| 553 |
+
"gaming": "gaming", "games": "gaming", "game": "gaming", "قيمز": "gaming", "العاب": "gaming",
|
| 554 |
+
"movies": "movies", "movie": "movies", "افلام": "movies", "أفلام": "movies",
|
| 555 |
+
"series": "series", "show": "series", "shows": "series", "مسلسلات": "series",
|
| 556 |
+
}
|
| 557 |
+
normalized = aliases.get(normalized, normalized) if normalized else random.choice(["gaming", "movies", "series"])
|
| 558 |
+
if normalized not in {"gaming", "movies", "series"}:
|
| 559 |
+
if lang == "ar":
|
| 560 |
+
await ctx.reply("❌ قسم غير مدعوم. استخدم: gaming أو movies أو series")
|
| 561 |
+
else:
|
| 562 |
+
await ctx.reply("❌ Unsupported category. Use: gaming, movies, or series")
|
| 563 |
+
return
|
| 564 |
+
|
| 565 |
+
# Get trivia from appropriate language bank
|
| 566 |
+
trivia_bank = TRIVIA_BANK.get(lang, TRIVIA_BANK["en"])
|
| 567 |
+
if normalized not in trivia_bank:
|
| 568 |
+
trivia_bank = TRIVIA_BANK["en"]
|
| 569 |
+
item = random.choice(trivia_bank[normalized])
|
| 570 |
+
|
| 571 |
+
view = TriviaView(owner_id=ctx.author.id, correct_index=int(item["a"]), lang=lang, cog=self, guild_id=guild_id)
|
| 572 |
+
for idx, choice in enumerate(item["c"]):
|
| 573 |
+
view.add_item(TriviaAnswerButton(str(choice), idx))
|
| 574 |
+
|
| 575 |
+
if lang == "ar":
|
| 576 |
+
title = "🧠 سؤال تراڤيا!"
|
| 577 |
+
cat_label = "📁 القسم"
|
| 578 |
+
topics_label = "📚 المواضيع"
|
| 579 |
+
else:
|
| 580 |
+
title = "🧠 Trivia Time!"
|
| 581 |
+
cat_label = "📁 Category"
|
| 582 |
+
topics_label = "📚 Topics"
|
| 583 |
+
|
| 584 |
+
embed = discord.Embed(
|
| 585 |
+
title=title,
|
| 586 |
+
description=f"{panel_divider('blue')}\n❓ {str(item['q'])}\n{panel_divider('blue')}",
|
| 587 |
+
color=NEON_CYAN,
|
| 588 |
+
)
|
| 589 |
+
cat_value = normalized.title() if normalized else ("General" if normalized_source == "api" else "Random")
|
| 590 |
+
embed.add_field(name=cat_label, value=cat_value, inline=True)
|
| 591 |
+
embed.add_field(name=topics_label, value=topic_text, inline=True)
|
| 592 |
+
await ctx.reply(embed=embed, view=view)
|
| 593 |
+
|
| 594 |
+
async def _fetch_gaming_news(self) -> list[dict[str, str]]:
|
| 595 |
+
return await fetch_gaming_news()
|
| 596 |
+
|
| 597 |
+
async def _fetch_free_games(self) -> list[dict[str, str]]:
|
| 598 |
+
return await fetch_free_games()
|
| 599 |
+
|
| 600 |
+
@staticmethod
|
| 601 |
+
def _store_icon(platform: str) -> str:
|
| 602 |
+
return store_icon(platform)
|
| 603 |
+
|
| 604 |
+
def _free_game_embed(self, item: dict[str, str]) -> tuple[discord.Embed, discord.ui.View]:
|
| 605 |
+
title = item["title"][:120]
|
| 606 |
+
link = item["link"]
|
| 607 |
+
embed = ImperialMotaz.craft_embed(
|
| 608 |
+
title=f"[FREE GAME] | {title}",
|
| 609 |
+
description=(
|
| 610 |
+
f"**{item.get('description', 'Limited-time free game offer!')}**\n\n"
|
| 611 |
+
"Claim it now from the official store page before it expires."
|
| 612 |
+
),
|
| 613 |
+
color=NEON_PINK,
|
| 614 |
+
footer="Free Games Tracker",
|
| 615 |
+
)
|
| 616 |
+
embed.url = link
|
| 617 |
+
embed.add_field(name="Platform", value=f"「 {item.get('platform', 'Unknown')} 」", inline=True)
|
| 618 |
+
embed.add_field(name="Type", value=f"「 {item.get('game_type', 'Game')} 」", inline=True)
|
| 619 |
+
embed.add_field(name="Original Price", value=f"「 {item.get('original_price', 'N/A')} 」", inline=True)
|
| 620 |
+
embed.add_field(name="Ends On", value=f"「 {item.get('end_date', 'N/A')} 」", inline=True)
|
| 621 |
+
embed.add_field(name="Link", value=f"[Open Giveaway]({link})", inline=False)
|
| 622 |
+
if item.get("image"):
|
| 623 |
+
embed.set_image(url=item["image"])
|
| 624 |
+
embed.set_thumbnail(url=self._store_icon(item.get("platform", "")))
|
| 625 |
+
return embed, FreeGameClaimView(link)
|
| 626 |
+
|
| 627 |
+
@commands.hybrid_command(name="gaming_news", description=get_cmd_desc("commands.tools.gaming_news_desc"))
|
| 628 |
+
async def gaming_news(self, ctx: commands.Context) -> None:
|
| 629 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 630 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 631 |
+
|
| 632 |
+
try:
|
| 633 |
+
items = await self._fetch_gaming_news()
|
| 634 |
+
except Exception:
|
| 635 |
+
if lang == "ar":
|
| 636 |
+
await ctx.reply("❌ تعذر جلب أخبار الألعاب الآن.")
|
| 637 |
+
else:
|
| 638 |
+
await ctx.reply("❌ Could not fetch gaming news right now.")
|
| 639 |
+
return
|
| 640 |
+
|
| 641 |
+
if not items:
|
| 642 |
+
if lang == "ar":
|
| 643 |
+
await ctx.reply("لا توجد أخبار متاحة الآن.")
|
| 644 |
+
else:
|
| 645 |
+
await ctx.reply("No gaming news available right now.")
|
| 646 |
+
return
|
| 647 |
+
|
| 648 |
+
if lang == "ar":
|
| 649 |
+
title = "🕹️ آخر أخبار الألعاب عالميًا"
|
| 650 |
+
else:
|
| 651 |
+
title = "🕹️ Latest Gaming News Worldwide"
|
| 652 |
+
|
| 653 |
+
embed = discord.Embed(
|
| 654 |
+
title=title,
|
| 655 |
+
description=f"{panel_divider('green')}",
|
| 656 |
+
color=NEON_CYAN,
|
| 657 |
+
)
|
| 658 |
+
for item in items:
|
| 659 |
+
date_hint = f"\n🗓️ {item['pub_date'][:16]}" if item.get("pub_date") else ""
|
| 660 |
+
if lang == "ar":
|
| 661 |
+
embed.add_field(name=item["title"][:256], value=f"📰 [اقرأ المقال]({item['link']}){date_hint}", inline=False)
|
| 662 |
+
else:
|
| 663 |
+
embed.add_field(name=item["title"][:256], value=f"📰 [Read Article]({item['link']}){date_hint}", inline=False)
|
| 664 |
+
await ctx.reply(embed=embed)
|
| 665 |
+
|
| 666 |
+
@commands.hybrid_command(name="freegames", description=get_cmd_desc("commands.tools.freegames_desc"), with_app_command=False)
|
| 667 |
+
async def freegames(self, ctx: commands.Context) -> None:
|
| 668 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 669 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 670 |
+
|
| 671 |
+
try:
|
| 672 |
+
items = await self._fetch_free_games()
|
| 673 |
+
except Exception:
|
| 674 |
+
if lang == "ar":
|
| 675 |
+
await ctx.reply("❌ تعذر جلب العروض الآن.")
|
| 676 |
+
else:
|
| 677 |
+
await ctx.reply("❌ Could not fetch free game offers right now.")
|
| 678 |
+
return
|
| 679 |
+
|
| 680 |
+
if not items:
|
| 681 |
+
if lang == "ar":
|
| 682 |
+
await ctx.reply("لا توجد عروض مجانية نشطة الآن.")
|
| 683 |
+
else:
|
| 684 |
+
await ctx.reply("No active free game offers right now.")
|
| 685 |
+
return
|
| 686 |
+
|
| 687 |
+
for item in items:
|
| 688 |
+
embed, view = self._free_game_embed(item)
|
| 689 |
+
await ctx.reply(embed=embed, view=view)
|
| 690 |
+
|
| 691 |
+
@commands.hybrid_command(name="free_games", description=get_cmd_desc("commands.tools.free_games_desc"))
|
| 692 |
+
async def free_games(self, ctx: commands.Context) -> None:
|
| 693 |
+
await self.freegames(ctx)
|
| 694 |
+
|
| 695 |
+
@commands.hybrid_group(name="gamehub", fallback="panel", description="Interactive game hub with temporary private rooms")
|
| 696 |
+
async def gamehub(self, ctx: commands.Context, invite: discord.Member | None = None) -> None:
|
| 697 |
+
if not ctx.guild:
|
| 698 |
+
await ctx.reply("Server only.")
|
| 699 |
+
return
|
| 700 |
+
|
| 701 |
+
guild_id = ctx.guild.id
|
| 702 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 703 |
+
|
| 704 |
+
if invite and invite.bot:
|
| 705 |
+
if lang == "ar":
|
| 706 |
+
await ctx.reply("❌ ادعُ عضواً حقيقياً، ليس بوت.")
|
| 707 |
+
else:
|
| 708 |
+
await ctx.reply("❌ Invite a real member, not a bot.")
|
| 709 |
+
return
|
| 710 |
+
if invite and invite.id == ctx.author.id:
|
| 711 |
+
if lang == "ar":
|
| 712 |
+
await ctx.reply("❌ ادعُ عضواً آخر.")
|
| 713 |
+
else:
|
| 714 |
+
await ctx.reply("❌ Invite another member.")
|
| 715 |
+
return
|
| 716 |
+
|
| 717 |
+
games_list = " • ".join([f"{g['emoji']} {g['name']}" for g in GAME_HUB_GAMES])
|
| 718 |
+
|
| 719 |
+
if lang == "ar":
|
| 720 |
+
title = "🎮 مركز الألعاب"
|
| 721 |
+
desc = (
|
| 722 |
+
f"{panel_divider('pink')}\n"
|
| 723 |
+
f"🕹️ اختر لعبة أدناه لإنشاء **غرفة خاصة**.\n"
|
| 724 |
+
f"👥 ادعُ صديقاً أو العب ضد البوت.\n"
|
| 725 |
+
f"🗑️ الغرفة تُحذف تلقائياً عند إغلاقها.\n"
|
| 726 |
+
f"{panel_divider('pink')}\n\n"
|
| 727 |
+
f"**الألعاب:** {games_list}"
|
| 728 |
+
)
|
| 729 |
+
invited_label = "🎯 العضو المدعو"
|
| 730 |
+
else:
|
| 731 |
+
title = "🎮 Game Hub"
|
| 732 |
+
desc = (
|
| 733 |
+
f"{panel_divider('pink')}\n"
|
| 734 |
+
f"🕹️ Select a game below to create a **private room**.\n"
|
| 735 |
+
f"👥 Invite a friend or play solo vs the bot.\n"
|
| 736 |
+
f"🗑️ The room auto-deletes when you close it.\n"
|
| 737 |
+
f"{panel_divider('pink')}\n\n"
|
| 738 |
+
f"**Games:** {games_list}"
|
| 739 |
+
)
|
| 740 |
+
invited_label = "🎯 Invited Member"
|
| 741 |
+
|
| 742 |
+
embed = discord.Embed(
|
| 743 |
+
title=title,
|
| 744 |
+
description=desc,
|
| 745 |
+
color=NEON_CYAN,
|
| 746 |
+
)
|
| 747 |
+
if invite:
|
| 748 |
+
embed.add_field(name=invited_label, value=invite.mention, inline=False)
|
| 749 |
+
await ctx.reply(embed=embed, view=GameHubView(self, invite.id if invite else None, lang))
|
| 750 |
+
|
| 751 |
+
@gamehub.command(name="xo", description="TicTacToe vs member or bot")
|
| 752 |
+
async def gamehub_xo(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 753 |
+
await self.xo(ctx, opponent=opponent)
|
| 754 |
+
|
| 755 |
+
@gamehub.command(name="mario", description="Mario/Pac-Man arcade challenge")
|
| 756 |
+
async def gamehub_mario(self, ctx: commands.Context) -> None:
|
| 757 |
+
await self.mario(ctx)
|
| 758 |
+
|
| 759 |
+
@gamehub.command(name="dice", description="Roll dice")
|
| 760 |
+
async def gamehub_dice(self, ctx: commands.Context, sides: int = 6) -> None:
|
| 761 |
+
await self.dice(ctx, sides=sides)
|
| 762 |
+
|
| 763 |
+
@gamehub.command(name="slots", description="Slot machine game")
|
| 764 |
+
async def gamehub_slots(self, ctx: commands.Context, bet: int = 50) -> None:
|
| 765 |
+
await self.slots(ctx, bet=bet)
|
| 766 |
+
|
| 767 |
+
@gamehub.command(name="trivia", description="Gaming/Movies/Series trivia")
|
| 768 |
+
async def gamehub_trivia(self, ctx: commands.Context, category: str = "gaming", difficulty: str = "medium") -> None:
|
| 769 |
+
await self.trivia(ctx, category=category)
|
| 770 |
+
|
| 771 |
+
@commands.hybrid_command(name="xo", description=get_cmd_desc("commands.tools.xo_desc"), hidden=True, with_app_command=False)
|
| 772 |
+
async def xo(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
|
| 773 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 774 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 775 |
+
|
| 776 |
+
if opponent and opponent.bot:
|
| 777 |
+
opponent = None
|
| 778 |
+
view = TicTacToeView(ctx.author, opponent)
|
| 779 |
+
second = opponent.mention if opponent else "🤖 Bot"
|
| 780 |
+
|
| 781 |
+
if lang == "ar":
|
| 782 |
+
msg = await ctx.reply(
|
| 783 |
+
f"❎ تيك تاك تو: {ctx.author.mention} (X) ضد {second} (O)\n"
|
| 784 |
+
f"🎮 الدور: {ctx.author.mention}",
|
| 785 |
+
view=view
|
| 786 |
+
)
|
| 787 |
+
else:
|
| 788 |
+
msg = await ctx.reply(
|
| 789 |
+
f"❎ TicTacToe: {ctx.author.mention} (X) vs {second} (O)\n"
|
| 790 |
+
f"🎮 Turn: {ctx.author.mention}",
|
| 791 |
+
view=view
|
| 792 |
+
)
|
| 793 |
+
if opponent is None:
|
| 794 |
+
await view._maybe_bot(msg)
|
| 795 |
+
|
| 796 |
+
@commands.hybrid_command(name="choose", hidden=True, with_app_command=False, description=get_cmd_desc("commands.tools.choose_desc"))
|
| 797 |
+
async def choose(self, ctx: commands.Context, *, options: str) -> None:
|
| 798 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 799 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 800 |
+
|
| 801 |
+
parts = [p.strip() for p in options.split(",") if p.strip()]
|
| 802 |
+
if len(parts) < 2:
|
| 803 |
+
if lang == "ar":
|
| 804 |
+
await ctx.reply("❌ أدخل خيارين على الأقل مفصولين بفواصل.")
|
| 805 |
+
else:
|
| 806 |
+
await ctx.reply("❌ Provide at least 2 options separated by commas.")
|
| 807 |
+
return
|
| 808 |
+
|
| 809 |
+
choice = random.choice(parts)
|
| 810 |
+
|
| 811 |
+
if lang == "ar":
|
| 812 |
+
embed = discord.Embed(
|
| 813 |
+
title="🎲 اختيار عشوائي",
|
| 814 |
+
description=(
|
| 815 |
+
f"{panel_divider('purple')}\n"
|
| 816 |
+
f"📝 الخيارات: {', '.join(parts)}\n"
|
| 817 |
+
f"🎯 **المختار:** {choice}\n"
|
| 818 |
+
f"{panel_divider('purple')}"
|
| 819 |
+
),
|
| 820 |
+
color=NEON_PINK,
|
| 821 |
+
)
|
| 822 |
+
else:
|
| 823 |
+
embed = discord.Embed(
|
| 824 |
+
title="🎲 Random Choice",
|
| 825 |
+
description=(
|
| 826 |
+
f"{panel_divider('purple')}\n"
|
| 827 |
+
f"📝 Options: {', '.join(parts)}\n"
|
| 828 |
+
f"🎯 **Selected:** {choice}\n"
|
| 829 |
+
f"{panel_divider('purple')}"
|
| 830 |
+
),
|
| 831 |
+
color=NEON_PINK,
|
| 832 |
+
)
|
| 833 |
+
await ctx.reply(embed=embed)
|
| 834 |
+
|
| 835 |
+
@commands.hybrid_command(name="mario", description=get_cmd_desc("commands.tools.mario_desc"), hidden=True, with_app_command=False)
|
| 836 |
+
async def mario(self, ctx: commands.Context) -> None:
|
| 837 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 838 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 839 |
+
|
| 840 |
+
coins = random.randint(5, 120)
|
| 841 |
+
level = random.randint(1, 8)
|
| 842 |
+
|
| 843 |
+
if lang == "ar":
|
| 844 |
+
embed = discord.Embed(
|
| 845 |
+
title="🍄 تحدي الآركيد",
|
| 846 |
+
description=(
|
| 847 |
+
f"{panel_divider('orange')}\n"
|
| 848 |
+
f"🍄 {ctx.author.mention} جمع **{coins}** عملة!\n"
|
| 849 |
+
f"✅ تجاوز المرحلة **{level}**!\n"
|
| 850 |
+
f"{panel_divider('orange')}"
|
| 851 |
+
),
|
| 852 |
+
color=NEON_ORANGE,
|
| 853 |
+
)
|
| 854 |
+
embed.add_field(name="🎁 مكافأة", value="تم فتح combo باك مان! <:animatedarrowyellow:1477261257592668271>", inline=False)
|
| 855 |
+
else:
|
| 856 |
+
embed = discord.Embed(
|
| 857 |
+
title="🍄 Arcade Challenge",
|
| 858 |
+
description=(
|
| 859 |
+
f"{panel_divider('orange')}\n"
|
| 860 |
+
f"🍄 {ctx.author.mention} collected **{coins}** coins!\n"
|
| 861 |
+
f"✅ Cleared level **{level}**!\n"
|
| 862 |
+
f"{panel_divider('orange')}"
|
| 863 |
+
),
|
| 864 |
+
color=NEON_ORANGE,
|
| 865 |
+
)
|
| 866 |
+
embed.add_field(name="🎁 Bonus", value="Pac-Man combo unlocked! <:animatedarrowyellow:1477261257592668271>", inline=False)
|
| 867 |
+
embed.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png")
|
| 868 |
+
await ctx.reply(embed=embed)
|
| 869 |
+
|
| 870 |
+
@commands.hybrid_command(name="dice", description=get_cmd_desc("commands.tools.dice_desc"), hidden=True, with_app_command=False)
|
| 871 |
+
async def dice(self, ctx: commands.Context, sides: int = 6) -> None:
|
| 872 |
+
"""Roll a dice with specified sides."""
|
| 873 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 874 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 875 |
+
|
| 876 |
+
sides = max(2, min(sides, 100))
|
| 877 |
+
result = random.randint(1, sides)
|
| 878 |
+
|
| 879 |
+
if lang == "ar":
|
| 880 |
+
embed = gaming_embed(
|
| 881 |
+
"🎲 رمي النرد",
|
| 882 |
+
f"🎲 رميت نرد بـ **{sides}** أوجه\n🎯 النتيجة: **{result}**"
|
| 883 |
+
)
|
| 884 |
+
else:
|
| 885 |
+
embed = gaming_embed(
|
| 886 |
+
"🎲 Dice Roll",
|
| 887 |
+
f"🎲 Rolled a **{sides}**-sided dice\n🎯 Result: **{result}**"
|
| 888 |
+
)
|
| 889 |
+
await ctx.reply(embed=embed)
|
| 890 |
+
|
| 891 |
+
@commands.hybrid_command(name="slots", description=get_cmd_desc("commands.tools.slots_desc"), hidden=True, with_app_command=False)
|
| 892 |
+
async def slots(self, ctx: commands.Context, bet: int = 10) -> None:
|
| 893 |
+
"""Play a slot machine game."""
|
| 894 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 895 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 896 |
+
|
| 897 |
+
bet = max(10, bet)
|
| 898 |
+
|
| 899 |
+
# Check balance
|
| 900 |
+
row = await self.bot.db.fetchone(
|
| 901 |
+
"SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
|
| 902 |
+
guild_id,
|
| 903 |
+
ctx.author.id,
|
| 904 |
+
)
|
| 905 |
+
wallet = row[0] if row else 0
|
| 906 |
+
|
| 907 |
+
if wallet < bet:
|
| 908 |
+
if lang == "ar":
|
| 909 |
+
await ctx.reply(f"❌ ليس لديك رصيد كافي. محفظتك: **{wallet}** عملة")
|
| 910 |
+
else:
|
| 911 |
+
await ctx.reply(f"❌ Insufficient balance. Your wallet: **{wallet}** coins")
|
| 912 |
+
return
|
| 913 |
+
|
| 914 |
+
# Slot symbols
|
| 915 |
+
symbols = ["🍒", "🍋", "🍊", "🍇", "✅", "💎", "7️⃣"]
|
| 916 |
+
weights = [25, 20, 18, 15, 12, 7, 3] # Weighted probabilities
|
| 917 |
+
|
| 918 |
+
# Spin
|
| 919 |
+
results = random.choices(symbols, weights=weights, k=3)
|
| 920 |
+
|
| 921 |
+
# Calculate winnings
|
| 922 |
+
if results[0] == results[1] == results[2]:
|
| 923 |
+
# Jackpot!
|
| 924 |
+
if results[0] == "7️⃣":
|
| 925 |
+
multiplier = 10
|
| 926 |
+
elif results[0] == "💎":
|
| 927 |
+
multiplier = 7
|
| 928 |
+
elif results[0] == "✅":
|
| 929 |
+
multiplier = 5
|
| 930 |
+
else:
|
| 931 |
+
multiplier = 3
|
| 932 |
+
winnings = bet * multiplier
|
| 933 |
+
elif results[0] == results[1] or results[1] == results[2] or results[0] == results[2]:
|
| 934 |
+
winnings = bet * 2
|
| 935 |
+
else:
|
| 936 |
+
winnings = 0
|
| 937 |
+
|
| 938 |
+
# Update balance
|
| 939 |
+
if winnings > 0:
|
| 940 |
+
await self._add_coins(guild_id, ctx.author.id, winnings - bet)
|
| 941 |
+
else:
|
| 942 |
+
await self._add_coins(guild_id, ctx.author.id, -bet)
|
| 943 |
+
|
| 944 |
+
# Build display
|
| 945 |
+
slot_display = " | ".join(results)
|
| 946 |
+
|
| 947 |
+
if winnings >= bet * 5:
|
| 948 |
+
result_text = f"🎰 **{'جائزة كبرى!' if lang == 'ar' else 'JACKPOT!'}** {E_FIRE}"
|
| 949 |
+
elif winnings > 0:
|
| 950 |
+
result_text = f"✅ **{'فزت!' if lang == 'ar' else 'You won!'}**"
|
| 951 |
+
else:
|
| 952 |
+
result_text = f"❌ **{'حظ أوفر' if lang == 'ar' else 'Better luck next time'}**"
|
| 953 |
+
|
| 954 |
+
if lang == "ar":
|
| 955 |
+
embed = gaming_embed(
|
| 956 |
+
"🎰 ماكينة القمار",
|
| 957 |
+
f"┌───────┐\n│ {slot_display} │\n└───────┘\n\n{result_text}\n💰 **الرابح:** {winnings} عملة"
|
| 958 |
+
)
|
| 959 |
+
else:
|
| 960 |
+
embed = gaming_embed(
|
| 961 |
+
"🎰 Slot Machine",
|
| 962 |
+
f"┌───────┐\n│ {slot_display} │\n└───────┘\n\n{result_text}\n💰 **Won:** {winnings} coins"
|
| 963 |
+
)
|
| 964 |
+
await ctx.reply(embed=embed)
|
| 965 |
+
|
| 966 |
+
async def _add_coins(self, guild_id: int, user_id: int, amount: int) -> None:
|
| 967 |
+
"""Helper to add/remove coins from user balance."""
|
| 968 |
+
if amount == 0:
|
| 969 |
+
return
|
| 970 |
+
await self.bot.db.execute(
|
| 971 |
+
"INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) "
|
| 972 |
+
"ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = wallet + ?",
|
| 973 |
+
guild_id,
|
| 974 |
+
user_id,
|
| 975 |
+
amount if amount > 0 else 0,
|
| 976 |
+
amount,
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
|
| 980 |
+
async def setup(bot: commands.Bot) -> None:
|
| 981 |
+
await bot.add_cog(Fun(bot))
|
bot/cogs/gambling.py
CHANGED
|
@@ -12,6 +12,7 @@ import discord
|
|
| 12 |
from discord.ext import commands
|
| 13 |
|
| 14 |
from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed
|
|
|
|
| 15 |
from bot.emojis import ui
|
| 16 |
|
| 17 |
|
|
@@ -282,7 +283,7 @@ class RouletteGameView(discord.ui.View):
|
|
| 282 |
await self.prompt_bet(interaction, "odd")
|
| 283 |
|
| 284 |
async def prompt_bet(self, interaction: discord.Interaction, bet_type: str):
|
| 285 |
-
if self.user_id and interaction.user.id != self.user_id:
|
| 286 |
return
|
| 287 |
await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type))
|
| 288 |
|
|
@@ -386,7 +387,7 @@ class RPGView(discord.ui.View):
|
|
| 386 |
|
| 387 |
@discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight")
|
| 388 |
async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 389 |
-
if self.user_id and interaction.user.id != self.user_id:
|
| 390 |
return
|
| 391 |
|
| 392 |
engagement_cog = interaction.client.get_cog("Engagement")
|
|
@@ -443,7 +444,7 @@ class RPGView(discord.ui.View):
|
|
| 443 |
|
| 444 |
@discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure")
|
| 445 |
async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 446 |
-
if self.user_id and interaction.user.id != self.user_id:
|
| 447 |
return
|
| 448 |
|
| 449 |
engagement_cog = interaction.client.get_cog("Engagement")
|
|
@@ -620,11 +621,11 @@ class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"):
|
|
| 620 |
await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True)
|
| 621 |
|
| 622 |
|
| 623 |
-
class Gambling(commands.Cog):
|
| 624 |
def __init__(self, bot: commands.Bot) -> None:
|
| 625 |
self.bot = bot
|
| 626 |
|
| 627 |
-
@commands.hybrid_command(name="blackjack", description="
|
| 628 |
async def blackjack(self, ctx: commands.Context, bet: int) -> None:
|
| 629 |
if bet < 10:
|
| 630 |
await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True)
|
|
@@ -650,13 +651,13 @@ class Gambling(commands.Cog):
|
|
| 650 |
message = await ctx.send(embed=embed, view=view)
|
| 651 |
view.message = message
|
| 652 |
|
| 653 |
-
@commands.hybrid_command(name="roulette", description="
|
| 654 |
async def roulette(self, ctx: commands.Context) -> None:
|
| 655 |
view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 656 |
embed = view.build_embed()
|
| 657 |
await ctx.send(embed=embed, view=view)
|
| 658 |
|
| 659 |
-
@commands.hybrid_command(name="rpg", description="
|
| 660 |
async def rpg(self, ctx: commands.Context) -> None:
|
| 661 |
view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 662 |
embed = view.build_embed(
|
|
@@ -666,7 +667,7 @@ class Gambling(commands.Cog):
|
|
| 666 |
)
|
| 667 |
await ctx.send(embed=embed, view=view, ephemeral=True)
|
| 668 |
|
| 669 |
-
@commands.hybrid_command(name="gambling_panel", description="
|
| 670 |
async def gambling_panel(self, ctx: commands.Context) -> None:
|
| 671 |
view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 672 |
embed = discord.Embed(
|
|
|
|
| 12 |
from discord.ext import commands
|
| 13 |
|
| 14 |
from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed
|
| 15 |
+
from bot.i18n import get_cmd_desc
|
| 16 |
from bot.emojis import ui
|
| 17 |
|
| 18 |
|
|
|
|
| 283 |
await self.prompt_bet(interaction, "odd")
|
| 284 |
|
| 285 |
async def prompt_bet(self, interaction: discord.Interaction, bet_type: str):
|
| 286 |
+
if self.user_id and interaction.user.id != self.user_id:
|
| 287 |
return
|
| 288 |
await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type))
|
| 289 |
|
|
|
|
| 387 |
|
| 388 |
@discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight")
|
| 389 |
async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 390 |
+
if self.user_id and interaction.user.id != self.user_id:
|
| 391 |
return
|
| 392 |
|
| 393 |
engagement_cog = interaction.client.get_cog("Engagement")
|
|
|
|
| 444 |
|
| 445 |
@discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure")
|
| 446 |
async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 447 |
+
if self.user_id and interaction.user.id != self.user_id:
|
| 448 |
return
|
| 449 |
|
| 450 |
engagement_cog = interaction.client.get_cog("Engagement")
|
|
|
|
| 621 |
await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True)
|
| 622 |
|
| 623 |
|
| 624 |
+
class Gambling(commands.Cog):
|
| 625 |
def __init__(self, bot: commands.Bot) -> None:
|
| 626 |
self.bot = bot
|
| 627 |
|
| 628 |
+
@commands.hybrid_command(name="blackjack", description=get_cmd_desc("commands.economy.blackjack_desc"))
|
| 629 |
async def blackjack(self, ctx: commands.Context, bet: int) -> None:
|
| 630 |
if bet < 10:
|
| 631 |
await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True)
|
|
|
|
| 651 |
message = await ctx.send(embed=embed, view=view)
|
| 652 |
view.message = message
|
| 653 |
|
| 654 |
+
@commands.hybrid_command(name="roulette", description=get_cmd_desc("commands.economy.roulette_desc"))
|
| 655 |
async def roulette(self, ctx: commands.Context) -> None:
|
| 656 |
view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 657 |
embed = view.build_embed()
|
| 658 |
await ctx.send(embed=embed, view=view)
|
| 659 |
|
| 660 |
+
@commands.hybrid_command(name="rpg", description=get_cmd_desc("commands.economy.rpg_desc"))
|
| 661 |
async def rpg(self, ctx: commands.Context) -> None:
|
| 662 |
view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 663 |
embed = view.build_embed(
|
|
|
|
| 667 |
)
|
| 668 |
await ctx.send(embed=embed, view=view, ephemeral=True)
|
| 669 |
|
| 670 |
+
@commands.hybrid_command(name="gambling_panel", description=get_cmd_desc("commands.economy.gambling_panel_desc"))
|
| 671 |
async def gambling_panel(self, ctx: commands.Context) -> None:
|
| 672 |
view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
| 673 |
embed = discord.Embed(
|
bot/cogs/language.py
CHANGED
|
@@ -1,187 +1,188 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
from bot.
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
"
|
| 15 |
-
"
|
| 16 |
-
"
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
self.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
await
|
| 73 |
-
await interaction.
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
"
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
await
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
f"{
|
| 140 |
-
f"
|
| 141 |
-
f"
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
embed.add_field(
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
"•
|
| 151 |
-
"•
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
@commands.
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
| 1 |
+
from bot.i18n import get_cmd_desc
|
| 2 |
+
"""
|
| 3 |
+
Language cog: Multi-language support for the bot.
|
| 4 |
+
Provides a beautiful panel for changing server language.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import discord
|
| 8 |
+
from discord.ext import commands
|
| 9 |
+
|
| 10 |
+
from bot.i18n import SUPPORTED_LANGUAGES, translate
|
| 11 |
+
from bot.theme import NEON_CYAN, NEON_LIME, panel_divider, success_embed, shimmer
|
| 12 |
+
|
| 13 |
+
LANGUAGE_META: dict[str, tuple[str, str]] = {
|
| 14 |
+
"ar": ("العربية", "🇸🇦"),
|
| 15 |
+
"en": ("English", "🇬🇧"),
|
| 16 |
+
"es": ("Español", "🇪🇸"),
|
| 17 |
+
"fr": ("Français", "🇫🇷"),
|
| 18 |
+
"de": ("Deutsch", "🇩🇪"),
|
| 19 |
+
"tr": ("Türkçe", "🇹🇷"),
|
| 20 |
+
"it": ("Italiano", "🇮🇹"),
|
| 21 |
+
"pt": ("Português", "🇵🇹"),
|
| 22 |
+
"ru": ("Русский", "🇷🇺"),
|
| 23 |
+
"hi": ("हिन्दी", "🇮🇳"),
|
| 24 |
+
"id": ("Indonesia", "🇮🇩"),
|
| 25 |
+
"ja": ("日本語", "🇯🇵"),
|
| 26 |
+
"zh": ("中文", "🇨🇳"),
|
| 27 |
+
"he": ("עברית", "🇮🇱"),
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class LanguageSelect(discord.ui.Select):
|
| 32 |
+
def __init__(self, cog: "Language", guild_id: int, current_code: str) -> None:
|
| 33 |
+
self.cog = cog
|
| 34 |
+
self.guild_id = guild_id
|
| 35 |
+
|
| 36 |
+
options: list[discord.SelectOption] = []
|
| 37 |
+
dynamic_supported = sorted(
|
| 38 |
+
getattr(getattr(cog.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES))
|
| 39 |
+
)
|
| 40 |
+
for code in dynamic_supported:
|
| 41 |
+
label, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
|
| 42 |
+
options.append(
|
| 43 |
+
discord.SelectOption(
|
| 44 |
+
label=label,
|
| 45 |
+
value=code,
|
| 46 |
+
emoji=emoji,
|
| 47 |
+
description=f"{code.upper()} language",
|
| 48 |
+
default=code == current_code,
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
super().__init__(
|
| 53 |
+
placeholder="Choose server language",
|
| 54 |
+
min_values=1,
|
| 55 |
+
max_values=1,
|
| 56 |
+
options=options[:25],
|
| 57 |
+
custom_id="lang:select",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
async def callback(self, interaction: discord.Interaction) -> None:
|
| 61 |
+
if not interaction.guild:
|
| 62 |
+
await interaction.response.send_message(translate("en", "common.server_only"), ephemeral=True)
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
selected = self.values[0]
|
| 66 |
+
|
| 67 |
+
# Special rule requested by owner: Hebrew is visible but cannot be set.
|
| 68 |
+
if selected == "he":
|
| 69 |
+
current_code = await self.cog._current_code(interaction.guild.id)
|
| 70 |
+
for option in self.options:
|
| 71 |
+
option.default = option.value == current_code
|
| 72 |
+
embed = await self.cog._language_embed(interaction.guild.id, current_code)
|
| 73 |
+
await interaction.response.edit_message(embed=embed, view=self.view)
|
| 74 |
+
await interaction.followup.send("only you know this language", ephemeral=True)
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
await self.cog.bot.db.execute(
|
| 78 |
+
"INSERT INTO guild_config(guild_id, guild_language) VALUES (?, ?) "
|
| 79 |
+
"ON CONFLICT(guild_id) DO UPDATE SET guild_language = excluded.guild_language",
|
| 80 |
+
interaction.guild.id,
|
| 81 |
+
selected,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
for option in self.options:
|
| 85 |
+
option.default = option.value == selected
|
| 86 |
+
|
| 87 |
+
embed = await self.cog._language_embed(interaction.guild.id, selected)
|
| 88 |
+
await interaction.response.edit_message(embed=embed, view=self.view)
|
| 89 |
+
|
| 90 |
+
# Send follow-up with hint to open /menu
|
| 91 |
+
lang_name = LANGUAGE_META.get(selected, (selected.upper(), "🌐"))[0]
|
| 92 |
+
hint_msg = translate(selected, "lang.updated")
|
| 93 |
+
|
| 94 |
+
followup_embed = discord.Embed(
|
| 95 |
+
title=f"✅ Language Changed!",
|
| 96 |
+
description=f"{panel_divider('lime')}\n{hint_msg}\n\n💡 Use `/menu` to see all commands in your selected language!\n{panel_divider('lime')}",
|
| 97 |
+
color=NEON_LIME,
|
| 98 |
+
)
|
| 99 |
+
await interaction.followup.send(embed=followup_embed, ephemeral=True)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class LanguagePanelView(discord.ui.View):
|
| 103 |
+
def __init__(self, cog: "Language", guild_id: int, current_code: str) -> None:
|
| 104 |
+
super().__init__(timeout=None)
|
| 105 |
+
self.add_item(LanguageSelect(cog, guild_id, current_code))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class Language(commands.Cog):
|
| 109 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 110 |
+
self.bot = bot
|
| 111 |
+
|
| 112 |
+
async def cog_load(self) -> None:
|
| 113 |
+
self.bot.add_view(LanguagePanelView(self, 0, "ar"))
|
| 114 |
+
|
| 115 |
+
async def _current_code(self, guild_id: int) -> str:
|
| 116 |
+
row = await self.bot.db.fetchone("SELECT guild_language FROM guild_config WHERE guild_id = ?", guild_id)
|
| 117 |
+
supported = set(getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES)))
|
| 118 |
+
return row[0] if row and row[0] in supported else "ar"
|
| 119 |
+
|
| 120 |
+
async def _language_embed(self, guild_id: int, current_code: str) -> discord.Embed:
|
| 121 |
+
current_name = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[0]
|
| 122 |
+
current_emoji = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[1]
|
| 123 |
+
|
| 124 |
+
lines = []
|
| 125 |
+
supported = sorted(
|
| 126 |
+
getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES))
|
| 127 |
+
)
|
| 128 |
+
for code in supported:
|
| 129 |
+
name, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
|
| 130 |
+
marker = "✅" if code == current_code else "▫️"
|
| 131 |
+
lines.append(f"{marker} {emoji} **{name}** `({code})`")
|
| 132 |
+
|
| 133 |
+
# Get translated content
|
| 134 |
+
title = translate(current_code, "lang.current").replace("{globe}", "🌍")
|
| 135 |
+
|
| 136 |
+
embed = discord.Embed(
|
| 137 |
+
title="🌍 Language Control Panel",
|
| 138 |
+
description=(
|
| 139 |
+
f"{shimmer('Current Language')}: **{current_emoji} {current_name}** (`{current_code}`)\n"
|
| 140 |
+
f"{panel_divider('cyan')}\n"
|
| 141 |
+
f"Pick your server language from the dropdown below.\n"
|
| 142 |
+
f"All bot messages and panels will use the selected language!"
|
| 143 |
+
),
|
| 144 |
+
color=NEON_CYAN,
|
| 145 |
+
)
|
| 146 |
+
embed.add_field(name="🌐 Supported Languages", value="\n".join(lines[:20]), inline=False)
|
| 147 |
+
embed.add_field(
|
| 148 |
+
name="💡 Tips",
|
| 149 |
+
value=(
|
| 150 |
+
"• Language changes apply instantly\n"
|
| 151 |
+
"• Use `/menu` to see commands in your language\n"
|
| 152 |
+
"• Available full locales: Arabic, English, Spanish, French, German"
|
| 153 |
+
),
|
| 154 |
+
inline=False
|
| 155 |
+
)
|
| 156 |
+
embed.set_footer(text="Changes apply instantly across bot messages and panels")
|
| 157 |
+
return embed
|
| 158 |
+
|
| 159 |
+
@commands.hybrid_command(name="language", description=get_cmd_desc("commands.tools.language_desc"))
|
| 160 |
+
@commands.has_permissions(manage_guild=True)
|
| 161 |
+
async def language(self, ctx: commands.Context) -> None:
|
| 162 |
+
if not ctx.guild:
|
| 163 |
+
await ctx.reply(translate("en", "common.server_only"))
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
current_code = await self._current_code(ctx.guild.id)
|
| 167 |
+
embed = await self._language_embed(ctx.guild.id, current_code)
|
| 168 |
+
view = LanguagePanelView(self, ctx.guild.id, current_code)
|
| 169 |
+
await ctx.reply(embed=embed, view=view)
|
| 170 |
+
|
| 171 |
+
@commands.hybrid_command(name="languages", description=get_cmd_desc("commands.tools.languages_desc"), hidden=True, with_app_command=False)
|
| 172 |
+
async def languages(self, ctx: commands.Context) -> None:
|
| 173 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 174 |
+
lang = await self.bot.get_guild_language(guild_id)
|
| 175 |
+
|
| 176 |
+
pretty = [f"{LANGUAGE_META.get(code, (code.upper(), '🌐'))[1]} **{LANGUAGE_META.get(code, (code.upper(), '🌐'))[0]}** (`{code}`)" for code in LANGUAGE_META]
|
| 177 |
+
|
| 178 |
+
embed = discord.Embed(
|
| 179 |
+
title="🌍 Supported Languages",
|
| 180 |
+
description=f"{panel_divider('cyan')}\n" + "\n".join(pretty) + f"\n{panel_divider('cyan')}",
|
| 181 |
+
color=NEON_CYAN,
|
| 182 |
+
)
|
| 183 |
+
embed.set_footer(text=f"Current: {LANGUAGE_META.get(lang, (lang.upper(), '🌐'))[0]}")
|
| 184 |
+
await ctx.reply(embed=embed)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
async def setup(bot: commands.Bot) -> None:
|
| 188 |
+
await bot.add_cog(Language(bot))
|
bot/cogs/media.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bot/cogs/media_helpers.py
CHANGED
|
@@ -362,9 +362,9 @@ class AutoRefreshMixin:
|
|
| 362 |
|
| 363 |
class FiltersPanelView(discord.ui.View, AutoRefreshMixin):
|
| 364 |
"""Separate Filters Panel with auto-refresh."""
|
| 365 |
-
|
| 366 |
def __init__(self, cog: "Media", guild_id: int) -> None:
|
| 367 |
-
discord.ui.View.__init__(self, timeout=
|
| 368 |
AutoRefreshMixin.__init__(self)
|
| 369 |
self.cog = cog
|
| 370 |
self.guild_id = guild_id
|
|
@@ -747,10 +747,15 @@ class MusicPanelView(discord.ui.View, AutoRefreshMixin):
|
|
| 747 |
async def _build_refresh_embed(self) -> discord.Embed:
|
| 748 |
"""Build refresh embed."""
|
| 749 |
return await self.cog._music_panel_embed(self.guild_id)
|
| 750 |
-
|
| 751 |
def _build_refresh_view(self) -> None:
|
| 752 |
-
"""
|
| 753 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
@safe_interaction
|
| 756 |
async def _join(self, interaction: discord.Interaction) -> None:
|
|
|
|
| 362 |
|
| 363 |
class FiltersPanelView(discord.ui.View, AutoRefreshMixin):
|
| 364 |
"""Separate Filters Panel with auto-refresh."""
|
| 365 |
+
|
| 366 |
def __init__(self, cog: "Media", guild_id: int) -> None:
|
| 367 |
+
discord.ui.View.__init__(self, timeout=None)
|
| 368 |
AutoRefreshMixin.__init__(self)
|
| 369 |
self.cog = cog
|
| 370 |
self.guild_id = guild_id
|
|
|
|
| 747 |
async def _build_refresh_embed(self) -> discord.Embed:
|
| 748 |
"""Build refresh embed."""
|
| 749 |
return await self.cog._music_panel_embed(self.guild_id)
|
| 750 |
+
|
| 751 |
def _build_refresh_view(self) -> None:
|
| 752 |
+
"""Skip rebuilding view on auto-refresh to prevent 'unknown view' errors.
|
| 753 |
+
|
| 754 |
+
The buttons are static and don't need rebuilding during auto-refresh.
|
| 755 |
+
Rebuilding clears items and creates new button instances which breaks
|
| 756 |
+
Discord's view caching and causes 'interaction referencing unknown view'.
|
| 757 |
+
"""
|
| 758 |
+
pass
|
| 759 |
|
| 760 |
@safe_interaction
|
| 761 |
async def _join(self, interaction: discord.Interaction) -> None:
|
bot/cogs/menu.py
CHANGED
|
@@ -9,6 +9,7 @@ import discord
|
|
| 9 |
from discord.ext import commands
|
| 10 |
|
| 11 |
from bot.cogs.ai_suite import ImperialMotaz
|
|
|
|
| 12 |
from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN
|
| 13 |
from bot.theme import (
|
| 14 |
fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider,
|
|
@@ -20,78 +21,93 @@ from bot.theme import (
|
|
| 20 |
|
| 21 |
|
| 22 |
# Beautiful unicode emojis for select menu categories
|
| 23 |
-
_CATEGORY_EMOJIS = {
|
| 24 |
-
"Music": "
|
| 25 |
-
"Admin": "
|
| 26 |
-
"Fun": "
|
| 27 |
-
"AI": "
|
| 28 |
-
"Utility": "
|
| 29 |
-
"Config": "
|
| 30 |
-
"Economy": "
|
| 31 |
-
"Moderation": "
|
| 32 |
-
"Tickets": "
|
| 33 |
-
"Welcome": "
|
| 34 |
-
"Giveaway": "
|
| 35 |
-
"Verification": "
|
| 36 |
-
"Tournament": "
|
| 37 |
-
"Games": "
|
| 38 |
-
"Level": "
|
| 39 |
-
"AutoMod": "
|
| 40 |
-
"Logs": "
|
| 41 |
-
"DJ": "
|
| 42 |
-
"Developer": "
|
| 43 |
-
"default": "
|
| 44 |
}
|
| 45 |
|
| 46 |
# Category descriptions with emojis
|
| 47 |
-
_CATEGORY_DESCRIPTIONS = {
|
| 48 |
-
"Music": "
|
| 49 |
-
"Admin": "
|
| 50 |
-
"Fun": "
|
| 51 |
-
"AI": "
|
| 52 |
-
"Utility": "
|
| 53 |
-
"Config": "
|
| 54 |
-
"Economy": "
|
| 55 |
-
"Moderation": "
|
| 56 |
-
"Tickets": "
|
| 57 |
-
"Welcome": "
|
| 58 |
-
"Giveaway": "
|
| 59 |
-
"Verification": "
|
| 60 |
-
"Tournament": "
|
| 61 |
-
"Games": "
|
| 62 |
-
"Level": "
|
| 63 |
-
"AutoMod": "
|
| 64 |
-
"Logs": "
|
| 65 |
-
"DJ": "
|
| 66 |
-
"Developer": "
|
| 67 |
}
|
| 68 |
|
| 69 |
_CATEGORY_BILINGUAL = {
|
| 70 |
-
"__all__": "
|
| 71 |
-
"__ai__": "
|
| 72 |
-
"Music": "
|
| 73 |
-
"Admin": "
|
| 74 |
-
"Fun": "
|
| 75 |
-
"AI": "
|
| 76 |
-
"Utility": "
|
| 77 |
-
"Config": "
|
| 78 |
-
"Economy": "
|
| 79 |
-
"Moderation": "
|
| 80 |
-
"Community": "
|
| 81 |
-
"AISuite": "
|
| 82 |
-
"Configuration": "
|
| 83 |
-
"Events": "
|
| 84 |
-
"Verification": "
|
| 85 |
}
|
| 86 |
|
| 87 |
|
| 88 |
def _bilingual_category(name: str) -> str:
|
| 89 |
-
return _CATEGORY_BILINGUAL.get(name, f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
class CommandsMenuSelect(discord.ui.Select):
|
| 93 |
"""Beautiful dropdown menu for selecting command categories."""
|
| 94 |
-
|
| 95 |
def __init__(
|
| 96 |
self,
|
| 97 |
cog_names: list[str],
|
|
@@ -121,23 +137,25 @@ class CommandsMenuSelect(discord.ui.Select):
|
|
| 121 |
default=selected_cog == "__ai__",
|
| 122 |
),
|
| 123 |
]
|
| 124 |
-
|
| 125 |
# Add cog options with beautiful emojis
|
| 126 |
for name in sorted(cog_names):
|
| 127 |
-
|
| 128 |
-
desc = cog_desc_map.get(name, _CATEGORY_DESCRIPTIONS.get(name, f"{name} commands"))
|
| 129 |
# Truncate description to fit Discord's limits
|
| 130 |
desc = desc[:100]
|
|
|
|
|
|
|
| 131 |
options.append(
|
| 132 |
discord.SelectOption(
|
| 133 |
label=_bilingual_category(name)[:100],
|
| 134 |
description=desc,
|
| 135 |
-
emoji=
|
| 136 |
value=name,
|
| 137 |
default=selected_cog == name,
|
| 138 |
)
|
| 139 |
)
|
| 140 |
-
|
| 141 |
super().__init__(
|
| 142 |
placeholder=placeholder,
|
| 143 |
min_values=1,
|
|
@@ -184,7 +202,7 @@ class CommandsMenuView(discord.ui.View):
|
|
| 184 |
key = f"menu.category_desc.{name.lower()}"
|
| 185 |
desc = await self.bot.get_text(self.guild_id, key)
|
| 186 |
if desc == key:
|
| 187 |
-
desc = _CATEGORY_DESCRIPTIONS.get(name, f"
|
| 188 |
cog_desc_map[name] = desc
|
| 189 |
self.clear_items()
|
| 190 |
self.add_item(
|
|
@@ -203,13 +221,13 @@ class CommandsMenuView(discord.ui.View):
|
|
| 203 |
refresh_label = await self.bot.get_text(self.guild_id, "menu.refresh")
|
| 204 |
invite_label = await self.bot.get_text(self.guild_id, "menu.invite_button")
|
| 205 |
self.add_item(RefreshButton(self, refresh_label))
|
| 206 |
-
self.add_item(QuickCategoryButton(self, "Economy |
|
| 207 |
-
self.add_item(QuickCategoryButton(self, "Music |
|
| 208 |
-
self.add_item(QuickCategoryButton(self, "Admin |
|
| 209 |
-
self.add_item(QuickCategoryButton(self, "Utility |
|
| 210 |
-
self.add_item(QuickCategoryButton(self, "Community |
|
| 211 |
-
self.add_item(QuickCategoryButton(self, "AI |
|
| 212 |
-
self.add_item(QuickCategoryButton(self, "Config |
|
| 213 |
if self.page > 0:
|
| 214 |
self.add_item(PageButton(self, "prev"))
|
| 215 |
if self._has_next_page(self.selected_cog):
|
|
@@ -348,6 +366,8 @@ class CommandsMenuView(discord.ui.View):
|
|
| 348 |
"reload": "menu.cmd.reload",
|
| 349 |
"sync": "menu.cmd.sync",
|
| 350 |
"shutdown": "menu.cmd.shutdown",
|
|
|
|
|
|
|
| 351 |
|
| 352 |
# Verification commands
|
| 353 |
"verify": "menu.cmd.verify",
|
|
@@ -400,55 +420,55 @@ class CommandsMenuView(discord.ui.View):
|
|
| 400 |
"roulette": "menu.cmd.roulette",
|
| 401 |
"rpg": "menu.cmd.rpg",
|
| 402 |
|
| 403 |
-
# AI Admin commands
|
| 404 |
-
"ai_admin": "menu.cmd.ai_admin",
|
| 405 |
-
"ai_help": "menu.cmd.ai_help",
|
| 406 |
-
# Additional commands/groups to keep menu fully mapped
|
| 407 |
-
"admin": "menu.cmd.admin_panel",
|
| 408 |
-
"admin_panel": "menu.cmd.admin_panel",
|
| 409 |
-
"shield_level": "menu.cmd.shield_level",
|
| 410 |
-
"shield_state": "menu.cmd.shield_state",
|
| 411 |
-
"econ_admin": "menu.cmd.econ_admin",
|
| 412 |
-
"econadmin": "menu.cmd.econ_admin",
|
| 413 |
-
"economy_admin": "menu.cmd.econ_admin",
|
| 414 |
-
"giveaway": "menu.cmd.giveaway",
|
| 415 |
-
"ticket": "menu.cmd.ticket",
|
| 416 |
-
"poll_legacy": "menu.cmd.poll",
|
| 417 |
-
"setpollchannel": "menu.cmd.set_pollchannel",
|
| 418 |
-
"setsuggestionchannel": "menu.cmd.set_suggestions",
|
| 419 |
-
"set_freegame": "menu.cmd.set_freegames",
|
| 420 |
-
"setupserver": "menu.cmd.setupserver",
|
| 421 |
-
"organizechannels": "menu.cmd.organizechannels",
|
| 422 |
-
"backup_panel": "menu.cmd.backup_panel",
|
| 423 |
-
"system_audit": "menu.cmd.system_audit",
|
| 424 |
-
"wisdom_today": "menu.cmd.wisdom_today",
|
| 425 |
-
"freegames": "menu.cmd.free_games",
|
| 426 |
-
"playlists": "menu.cmd.music_playlist",
|
| 427 |
-
"music": "menu.cmd.music_panel",
|
| 428 |
-
"profile": "menu.cmd.profile",
|
| 429 |
-
"rps": "menu.cmd.rps",
|
| 430 |
-
"guess": "menu.cmd.guess",
|
| 431 |
-
"make_event": "menu.cmd.make_event",
|
| 432 |
-
"gambling_panel": "menu.cmd.gambling_panel",
|
| 433 |
-
"add_scam_image": "menu.cmd.add_scam_image",
|
| 434 |
-
"set_banner": "menu.cmd.set_banner",
|
| 435 |
-
"view_banner": "menu.cmd.view_banner",
|
| 436 |
-
"remove_banner": "menu.cmd.remove_banner",
|
| 437 |
-
"banner_help": "menu.cmd.banner_help",
|
| 438 |
-
"boardgames": "menu.cmd.boardgames",
|
| 439 |
-
"board_start": "menu.cmd.board_start",
|
| 440 |
-
"board_move": "menu.cmd.board_move",
|
| 441 |
-
"board_forfeit": "menu.cmd.board_forfeit",
|
| 442 |
-
"games_panel": "menu.cmd.games_panel",
|
| 443 |
-
"chess": "menu.cmd.chess",
|
| 444 |
-
"checkers": "menu.cmd.checkers",
|
| 445 |
-
"connect4": "menu.cmd.connect4",
|
| 446 |
-
"othello": "menu.cmd.othello",
|
| 447 |
-
"start": "menu.cmd.menu",
|
| 448 |
-
"command_fill": "menu.cmd.menu",
|
| 449 |
-
"tournament_lb": "menu.cmd.tournament_lb",
|
| 450 |
-
"aisetup": "menu.cmd.ai",
|
| 451 |
-
}
|
| 452 |
return key_mapping.get(norm, f"menu.cmd.{norm}")
|
| 453 |
|
| 454 |
async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str:
|
|
@@ -471,13 +491,13 @@ class CommandsMenuView(discord.ui.View):
|
|
| 471 |
|
| 472 |
if translated_desc == desc_key or (json_key and translated_desc == json_key):
|
| 473 |
fallback_desc = (cmd.description or cmd.help or "").strip()
|
| 474 |
-
desc = fallback_desc if fallback_desc else "-"
|
| 475 |
else:
|
| 476 |
desc = translated_desc
|
| 477 |
|
| 478 |
is_hybrid = isinstance(cmd, commands.HybridCommand)
|
| 479 |
invoke = f"`/{cmd.qualified_name}`" if is_hybrid else f"`!{cmd.qualified_name}`"
|
| 480 |
-
line = f"- {invoke} - {desc[:78]}"
|
| 481 |
projected = used + len(line) + (1 if lines else 0)
|
| 482 |
|
| 483 |
if projected > max_chars:
|
|
@@ -488,12 +508,12 @@ class CommandsMenuView(discord.ui.View):
|
|
| 488 |
if len(lines) >= max_lines:
|
| 489 |
break
|
| 490 |
|
| 491 |
-
if len(lines) < len([c for c in cmds if not c.hidden]):
|
| 492 |
-
suffix = "\n- ------------"
|
| 493 |
-
if used + len(suffix) <= max_chars:
|
| 494 |
-
lines.append("- ------------")
|
| 495 |
-
|
| 496 |
-
return "\n".join(lines)
|
| 497 |
|
| 498 |
def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]:
|
| 499 |
commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name)
|
|
@@ -527,16 +547,16 @@ class CommandsMenuView(discord.ui.View):
|
|
| 527 |
|
| 528 |
lines: list[str] = []
|
| 529 |
for index, (name, count) in enumerate(stats[:8], start=1):
|
| 530 |
-
badge = {1: "
|
| 531 |
-
emoji = _CATEGORY_EMOJIS.get(name, "
|
| 532 |
|
| 533 |
if lang == "ar":
|
| 534 |
-
lines.append(f"{badge} {emoji} **{name}**
|
| 535 |
else:
|
| 536 |
-
lines.append(f"{badge} {emoji} **{name}**
|
| 537 |
|
| 538 |
if len(stats) > 8:
|
| 539 |
-
lines.append("
|
| 540 |
|
| 541 |
return "\n".join(lines)
|
| 542 |
|
|
@@ -553,17 +573,17 @@ class CommandsMenuView(discord.ui.View):
|
|
| 553 |
|
| 554 |
if lang == "ar":
|
| 555 |
pieces = [
|
| 556 |
-
f"
|
| 557 |
-
f"
|
| 558 |
-
f"
|
| 559 |
-
f"
|
| 560 |
]
|
| 561 |
else:
|
| 562 |
pieces = [
|
| 563 |
-
f"
|
| 564 |
-
f"
|
| 565 |
-
f"
|
| 566 |
-
f"
|
| 567 |
]
|
| 568 |
return "\n".join(pieces)
|
| 569 |
|
|
@@ -622,21 +642,21 @@ class CommandsMenuView(discord.ui.View):
|
|
| 622 |
f"? {await self.bot.get_text(guild_id, 'menu.tip_line_2')}\n"
|
| 623 |
f"? {await self.bot.get_text(guild_id, 'menu.tip_line_3')}"
|
| 624 |
)
|
| 625 |
-
quick = (
|
| 626 |
-
f"- `/music_panel` - {await self.bot.get_text(guild_id, 'menu.quick_music')}\n"
|
| 627 |
-
f"- `/gamehub` - {await self.bot.get_text(guild_id, 'menu.quick_gamehub')}\n"
|
| 628 |
-
f"- `/tournament panel` - {await self.bot.get_text(guild_id, 'menu.quick_tournament')}\n"
|
| 629 |
-
f"- `/economy` - {await self.bot.get_text(guild_id, 'menu.quick_economy')}"
|
| 630 |
-
)
|
| 631 |
-
quick_ai = await self.bot.get_text(guild_id, 'menu.quick_ai')
|
| 632 |
-
updates = (
|
| 633 |
-
f"- `/admin emoji clone` - {await self.bot.get_text(guild_id, 'menu.update_cloneemoji')}\n"
|
| 634 |
-
f"- `/admin shield add_image` - {await self.bot.get_text(guild_id, 'menu.update_shield_image')}\n"
|
| 635 |
-
f"- `/poll create` - {await self.bot.get_text(guild_id, 'menu.update_poll_group')}\n"
|
| 636 |
-
f"- `/economy deposit` - {await self.bot.get_text(guild_id, 'menu.update_economy_group')}\n"
|
| 637 |
-
f"- `/ai execute` - {quick_ai if quick_ai != 'menu.quick_ai' else 'AI admin request'}"
|
| 638 |
-
)
|
| 639 |
-
footer = "Powered by BOT- AI Suite"
|
| 640 |
|
| 641 |
top_divider = panel_divider("cyan")
|
| 642 |
mid_divider = panel_divider("purple")
|
|
@@ -662,10 +682,10 @@ class CommandsMenuView(discord.ui.View):
|
|
| 662 |
slash_budget = f"{slash_used}/100"
|
| 663 |
stats_grid = quick_stats_grid(
|
| 664 |
[
|
| 665 |
-
("Guilds", str(total_guilds), "G"),
|
| 666 |
-
("Members", f"{total_members:,}", "M"),
|
| 667 |
-
("Latency", latency_ms, "L"),
|
| 668 |
-
("Slash", slash_budget, "S"),
|
| 669 |
],
|
| 670 |
columns=2,
|
| 671 |
)
|
|
@@ -677,7 +697,7 @@ class CommandsMenuView(discord.ui.View):
|
|
| 677 |
inline=True,
|
| 678 |
)
|
| 679 |
|
| 680 |
-
emoji = _CATEGORY_EMOJIS.get(selected_cog, "C")
|
| 681 |
none_text = await self.bot.get_text(guild_id, "menu.none")
|
| 682 |
embed.add_field(
|
| 683 |
name=f"{emoji} {selected_label}",
|
|
@@ -690,7 +710,7 @@ class CommandsMenuView(discord.ui.View):
|
|
| 690 |
embed.add_field(name=updates_title, value=updates, inline=False)
|
| 691 |
|
| 692 |
embed.set_footer(text=footer)
|
| 693 |
-
embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}"
|
| 694 |
return embed
|
| 695 |
|
| 696 |
|
|
@@ -724,8 +744,8 @@ class RefreshButton(discord.ui.Button):
|
|
| 724 |
|
| 725 |
async def callback(self, interaction: discord.Interaction) -> None:
|
| 726 |
await interaction.response.defer()
|
| 727 |
-
guild_id = interaction.guild.id if interaction.guild else None
|
| 728 |
-
self.label = await self.parent_view.bot.get_text(guild_id, "menu.refresh")
|
| 729 |
await self.parent_view.setup_items()
|
| 730 |
embed = await self.parent_view.build_embed(guild_id, "__all__")
|
| 731 |
await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
|
|
@@ -757,7 +777,7 @@ class PageButton(discord.ui.Button):
|
|
| 757 |
self.parent_view = parent
|
| 758 |
self.direction = direction
|
| 759 |
label = "Previous" if direction == "prev" else "Next"
|
| 760 |
-
emoji = "
|
| 761 |
super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary)
|
| 762 |
|
| 763 |
async def callback(self, interaction: discord.Interaction) -> None:
|
|
@@ -778,10 +798,10 @@ class Menu(commands.Cog):
|
|
| 778 |
def __init__(self, bot: commands.Bot) -> None:
|
| 779 |
self.bot = bot
|
| 780 |
|
| 781 |
-
async def cog_load(self) -> None:
|
| 782 |
-
self.bot.add_view(CommandsMenuView(self.bot))
|
| 783 |
|
| 784 |
-
@commands.hybrid_command(name="menu", description="
|
| 785 |
async def menu(self, ctx: commands.Context) -> None:
|
| 786 |
"""Display the beautiful command menu."""
|
| 787 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
@@ -795,16 +815,16 @@ class Menu(commands.Cog):
|
|
| 795 |
else:
|
| 796 |
await ctx.reply(embed=embed, view=view)
|
| 797 |
|
| 798 |
-
@commands.hybrid_command(name="start", description="
|
| 799 |
async def start_menu(self, ctx: commands.Context) -> None:
|
| 800 |
await self.menu(ctx)
|
| 801 |
|
| 802 |
async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
| 803 |
embed = ImperialMotaz.craft_embed(
|
| 804 |
-
title="
|
| 805 |
-
description=f"
|
| 806 |
color=discord.Color(0x2B2D31),
|
| 807 |
-
footer="
|
| 808 |
)
|
| 809 |
try:
|
| 810 |
await ctx.reply(embed=embed)
|
|
|
|
| 9 |
from discord.ext import commands
|
| 10 |
|
| 11 |
from bot.cogs.ai_suite import ImperialMotaz
|
| 12 |
+
from bot.i18n import get_cmd_desc
|
| 13 |
from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN
|
| 14 |
from bot.theme import (
|
| 15 |
fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider,
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
# Beautiful unicode emojis for select menu categories
|
| 24 |
+
_CATEGORY_EMOJIS: dict[str, str] = {
|
| 25 |
+
"Music": "🎵",
|
| 26 |
+
"Admin": "🛡️",
|
| 27 |
+
"Fun": "🎮",
|
| 28 |
+
"AI": "🤖",
|
| 29 |
+
"Utility": "🔧",
|
| 30 |
+
"Config": "⚙️",
|
| 31 |
+
"Economy": "💰",
|
| 32 |
+
"Moderation": "⚔️",
|
| 33 |
+
"Tickets": "🎫",
|
| 34 |
+
"Welcome": "👋",
|
| 35 |
+
"Giveaway": "🎁",
|
| 36 |
+
"Verification": "✅",
|
| 37 |
+
"Tournament": "🏆",
|
| 38 |
+
"Games": "🎲",
|
| 39 |
+
"Level": "📊",
|
| 40 |
+
"AutoMod": "🤖",
|
| 41 |
+
"Logs": "📋",
|
| 42 |
+
"DJ": "🎧",
|
| 43 |
+
"Developer": "💻",
|
| 44 |
+
"default": "📂",
|
| 45 |
}
|
| 46 |
|
| 47 |
# Category descriptions with emojis
|
| 48 |
+
_CATEGORY_DESCRIPTIONS: dict[str, str] = {
|
| 49 |
+
"Music": "🎵 Music commands for playback control",
|
| 50 |
+
"Admin": "🛡️ Server administration tools",
|
| 51 |
+
"Fun": "🎮 Fun games and entertainment",
|
| 52 |
+
"AI": "🤖 AI-powered features",
|
| 53 |
+
"Utility": "🔧 Useful utility commands",
|
| 54 |
+
"Config": "⚙️ Bot configuration settings",
|
| 55 |
+
"Economy": "💰 Economy and currency system",
|
| 56 |
+
"Moderation": "⚔️ Moderation commands",
|
| 57 |
+
"Tickets": "🎫 Ticket support system",
|
| 58 |
+
"Welcome": "👋 Welcome and goodbye messages",
|
| 59 |
+
"Giveaway": "🎁 Giveaway management",
|
| 60 |
+
"Verification": "✅ Member verification",
|
| 61 |
+
"Tournament": "🏆 Tournament brackets",
|
| 62 |
+
"Games": "🎲 Mini games and fun",
|
| 63 |
+
"Level": "📊 XP and leveling system",
|
| 64 |
+
"AutoMod": "🤖 Auto-moderation features",
|
| 65 |
+
"Logs": "📋 Logging configuration",
|
| 66 |
+
"DJ": "🎧 DJ music controls",
|
| 67 |
+
"Developer": "💻 Developer tools",
|
| 68 |
}
|
| 69 |
|
| 70 |
_CATEGORY_BILINGUAL = {
|
| 71 |
+
"__all__": "📚 All Commands | جميع الأوامر",
|
| 72 |
+
"__ai__": "🤖 AI | الذكاء الاصطناعي",
|
| 73 |
+
"Music": "🎵 Music | الموسيقى",
|
| 74 |
+
"Admin": "🛡️ Admin | الإدارة",
|
| 75 |
+
"Fun": "🎮 Games | الألعاب",
|
| 76 |
+
"AI": "🤖 AI | الذكاء الاصطناعي",
|
| 77 |
+
"Utility": "🔧 Utility | الأدوات",
|
| 78 |
+
"Config": "⚙️ Config | الإعدادات",
|
| 79 |
+
"Economy": "💰 Economy | الاقتصاد",
|
| 80 |
+
"Moderation": "⚔️ Moderation | الإشراف",
|
| 81 |
+
"Community": "💡 Community | المجتمع",
|
| 82 |
+
"AISuite": "🤖 AI Suite | الذكاء",
|
| 83 |
+
"Configuration": "⚙️ Configuration | الإعدادات",
|
| 84 |
+
"Events": "📋 Events | الأحداث",
|
| 85 |
+
"Verification": "✅ Verification | التحقق",
|
| 86 |
}
|
| 87 |
|
| 88 |
|
| 89 |
def _bilingual_category(name: str) -> str:
|
| 90 |
+
return _CATEGORY_BILINGUAL.get(name, f"📂 {name} | {name}")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _safe_emoji(value: str) -> "discord.Emoji | discord.PartialEmoji | str | None":
|
| 94 |
+
"""Safely resolve an emoji value for use in Discord SelectOptions."""
|
| 95 |
+
if not value:
|
| 96 |
+
return None
|
| 97 |
+
value = value.strip()
|
| 98 |
+
if not value:
|
| 99 |
+
return None
|
| 100 |
+
if value.startswith("<") and value.endswith(">"):
|
| 101 |
+
try:
|
| 102 |
+
return discord.PartialEmoji.from_str(value)
|
| 103 |
+
except Exception:
|
| 104 |
+
return None
|
| 105 |
+
return value
|
| 106 |
|
| 107 |
|
| 108 |
class CommandsMenuSelect(discord.ui.Select):
|
| 109 |
"""Beautiful dropdown menu for selecting command categories."""
|
| 110 |
+
|
| 111 |
def __init__(
|
| 112 |
self,
|
| 113 |
cog_names: list[str],
|
|
|
|
| 137 |
default=selected_cog == "__ai__",
|
| 138 |
),
|
| 139 |
]
|
| 140 |
+
|
| 141 |
# Add cog options with beautiful emojis
|
| 142 |
for name in sorted(cog_names):
|
| 143 |
+
raw_emoji = _CATEGORY_EMOJIS.get(name, _CATEGORY_EMOJIS["default"])
|
| 144 |
+
desc = cog_desc_map.get(name, _CATEGORY_DESCRIPTIONS.get(name, f"{name} commands"))
|
| 145 |
# Truncate description to fit Discord's limits
|
| 146 |
desc = desc[:100]
|
| 147 |
+
# Safely resolve emoji - use PartialEmoji for custom emoji strings
|
| 148 |
+
resolved_emoji = _safe_emoji(raw_emoji)
|
| 149 |
options.append(
|
| 150 |
discord.SelectOption(
|
| 151 |
label=_bilingual_category(name)[:100],
|
| 152 |
description=desc,
|
| 153 |
+
emoji=resolved_emoji,
|
| 154 |
value=name,
|
| 155 |
default=selected_cog == name,
|
| 156 |
)
|
| 157 |
)
|
| 158 |
+
|
| 159 |
super().__init__(
|
| 160 |
placeholder=placeholder,
|
| 161 |
min_values=1,
|
|
|
|
| 202 |
key = f"menu.category_desc.{name.lower()}"
|
| 203 |
desc = await self.bot.get_text(self.guild_id, key)
|
| 204 |
if desc == key:
|
| 205 |
+
desc = _CATEGORY_DESCRIPTIONS.get(name, f"📂 {name} commands")
|
| 206 |
cog_desc_map[name] = desc
|
| 207 |
self.clear_items()
|
| 208 |
self.add_item(
|
|
|
|
| 221 |
refresh_label = await self.bot.get_text(self.guild_id, "menu.refresh")
|
| 222 |
invite_label = await self.bot.get_text(self.guild_id, "menu.invite_button")
|
| 223 |
self.add_item(RefreshButton(self, refresh_label))
|
| 224 |
+
self.add_item(QuickCategoryButton(self, "Economy | الاقتصاد", "Economy", "💰", row=1))
|
| 225 |
+
self.add_item(QuickCategoryButton(self, "Music | الموسيقى", "Music", "🎵", row=1))
|
| 226 |
+
self.add_item(QuickCategoryButton(self, "Admin | الإدارة", "Admin", "🛡️", row=1))
|
| 227 |
+
self.add_item(QuickCategoryButton(self, "Utility | الأدوات", "Utility", "ℹ️", row=1))
|
| 228 |
+
self.add_item(QuickCategoryButton(self, "Community | المجتمع", "Community", "💡", row=2))
|
| 229 |
+
self.add_item(QuickCategoryButton(self, "AI | الذكاء", "AISuite", "🤖", row=2))
|
| 230 |
+
self.add_item(QuickCategoryButton(self, "Config | الإعداد", "Configuration", "⚙️", row=2))
|
| 231 |
if self.page > 0:
|
| 232 |
self.add_item(PageButton(self, "prev"))
|
| 233 |
if self._has_next_page(self.selected_cog):
|
|
|
|
| 366 |
"reload": "menu.cmd.reload",
|
| 367 |
"sync": "menu.cmd.sync",
|
| 368 |
"shutdown": "menu.cmd.shutdown",
|
| 369 |
+
"bot_emojis": "menu.cmd.emoji_scan",
|
| 370 |
+
"emoji_scan": "menu.cmd.emoji_scan",
|
| 371 |
|
| 372 |
# Verification commands
|
| 373 |
"verify": "menu.cmd.verify",
|
|
|
|
| 420 |
"roulette": "menu.cmd.roulette",
|
| 421 |
"rpg": "menu.cmd.rpg",
|
| 422 |
|
| 423 |
+
# AI Admin commands
|
| 424 |
+
"ai_admin": "menu.cmd.ai_admin",
|
| 425 |
+
"ai_help": "menu.cmd.ai_help",
|
| 426 |
+
# Additional commands/groups to keep menu fully mapped
|
| 427 |
+
"admin": "menu.cmd.admin_panel",
|
| 428 |
+
"admin_panel": "menu.cmd.admin_panel",
|
| 429 |
+
"shield_level": "menu.cmd.shield_level",
|
| 430 |
+
"shield_state": "menu.cmd.shield_state",
|
| 431 |
+
"econ_admin": "menu.cmd.econ_admin",
|
| 432 |
+
"econadmin": "menu.cmd.econ_admin",
|
| 433 |
+
"economy_admin": "menu.cmd.econ_admin",
|
| 434 |
+
"giveaway": "menu.cmd.giveaway",
|
| 435 |
+
"ticket": "menu.cmd.ticket",
|
| 436 |
+
"poll_legacy": "menu.cmd.poll",
|
| 437 |
+
"setpollchannel": "menu.cmd.set_pollchannel",
|
| 438 |
+
"setsuggestionchannel": "menu.cmd.set_suggestions",
|
| 439 |
+
"set_freegame": "menu.cmd.set_freegames",
|
| 440 |
+
"setupserver": "menu.cmd.setupserver",
|
| 441 |
+
"organizechannels": "menu.cmd.organizechannels",
|
| 442 |
+
"backup_panel": "menu.cmd.backup_panel",
|
| 443 |
+
"system_audit": "menu.cmd.system_audit",
|
| 444 |
+
"wisdom_today": "menu.cmd.wisdom_today",
|
| 445 |
+
"freegames": "menu.cmd.free_games",
|
| 446 |
+
"playlists": "menu.cmd.music_playlist",
|
| 447 |
+
"music": "menu.cmd.music_panel",
|
| 448 |
+
"profile": "menu.cmd.profile",
|
| 449 |
+
"rps": "menu.cmd.rps",
|
| 450 |
+
"guess": "menu.cmd.guess",
|
| 451 |
+
"make_event": "menu.cmd.make_event",
|
| 452 |
+
"gambling_panel": "menu.cmd.gambling_panel",
|
| 453 |
+
"add_scam_image": "menu.cmd.add_scam_image",
|
| 454 |
+
"set_banner": "menu.cmd.set_banner",
|
| 455 |
+
"view_banner": "menu.cmd.view_banner",
|
| 456 |
+
"remove_banner": "menu.cmd.remove_banner",
|
| 457 |
+
"banner_help": "menu.cmd.banner_help",
|
| 458 |
+
"boardgames": "menu.cmd.boardgames",
|
| 459 |
+
"board_start": "menu.cmd.board_start",
|
| 460 |
+
"board_move": "menu.cmd.board_move",
|
| 461 |
+
"board_forfeit": "menu.cmd.board_forfeit",
|
| 462 |
+
"games_panel": "menu.cmd.games_panel",
|
| 463 |
+
"chess": "menu.cmd.chess",
|
| 464 |
+
"checkers": "menu.cmd.checkers",
|
| 465 |
+
"connect4": "menu.cmd.connect4",
|
| 466 |
+
"othello": "menu.cmd.othello",
|
| 467 |
+
"start": "menu.cmd.menu",
|
| 468 |
+
"command_fill": "menu.cmd.menu",
|
| 469 |
+
"tournament_lb": "menu.cmd.tournament_lb",
|
| 470 |
+
"aisetup": "menu.cmd.ai",
|
| 471 |
+
}
|
| 472 |
return key_mapping.get(norm, f"menu.cmd.{norm}")
|
| 473 |
|
| 474 |
async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str:
|
|
|
|
| 491 |
|
| 492 |
if translated_desc == desc_key or (json_key and translated_desc == json_key):
|
| 493 |
fallback_desc = (cmd.description or cmd.help or "").strip()
|
| 494 |
+
desc = fallback_desc if fallback_desc else "-"
|
| 495 |
else:
|
| 496 |
desc = translated_desc
|
| 497 |
|
| 498 |
is_hybrid = isinstance(cmd, commands.HybridCommand)
|
| 499 |
invoke = f"`/{cmd.qualified_name}`" if is_hybrid else f"`!{cmd.qualified_name}`"
|
| 500 |
+
line = f"- {invoke} - {desc[:78]}"
|
| 501 |
projected = used + len(line) + (1 if lines else 0)
|
| 502 |
|
| 503 |
if projected > max_chars:
|
|
|
|
| 508 |
if len(lines) >= max_lines:
|
| 509 |
break
|
| 510 |
|
| 511 |
+
if len(lines) < len([c for c in cmds if not c.hidden]):
|
| 512 |
+
suffix = "\n- ------------"
|
| 513 |
+
if used + len(suffix) <= max_chars:
|
| 514 |
+
lines.append("- ------------")
|
| 515 |
+
|
| 516 |
+
return "\n".join(lines)
|
| 517 |
|
| 518 |
def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]:
|
| 519 |
commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name)
|
|
|
|
| 547 |
|
| 548 |
lines: list[str] = []
|
| 549 |
for index, (name, count) in enumerate(stats[:8], start=1):
|
| 550 |
+
badge = {1: "🥇", 2: "🥈", 3: "🥉"}.get(index, f"#{index}")
|
| 551 |
+
emoji = _CATEGORY_EMOJIS.get(name, "📂")
|
| 552 |
|
| 553 |
if lang == "ar":
|
| 554 |
+
lines.append(f"{badge} {emoji} **{name}** — `{count}` أمر")
|
| 555 |
else:
|
| 556 |
+
lines.append(f"{badge} {emoji} **{name}** — `{count}` cmds")
|
| 557 |
|
| 558 |
if len(stats) > 8:
|
| 559 |
+
lines.append("✦ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈")
|
| 560 |
|
| 561 |
return "\n".join(lines)
|
| 562 |
|
|
|
|
| 573 |
|
| 574 |
if lang == "ar":
|
| 575 |
pieces = [
|
| 576 |
+
f"📊 **إجمالي الأوامر:** `{total_commands}`",
|
| 577 |
+
f"📂 **الفئات:** `{category_count}`",
|
| 578 |
+
f"✨ **المحدد:** {selection_label} (`{visible_commands}` أمر)",
|
| 579 |
+
f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
|
| 580 |
]
|
| 581 |
else:
|
| 582 |
pieces = [
|
| 583 |
+
f"📊 **Total Commands:** `{total_commands}`",
|
| 584 |
+
f"📂 **Categories:** `{category_count}`",
|
| 585 |
+
f"✨ **Selected:** {selection_label} (`{visible_commands}` cmds)",
|
| 586 |
+
f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
|
| 587 |
]
|
| 588 |
return "\n".join(pieces)
|
| 589 |
|
|
|
|
| 642 |
f"? {await self.bot.get_text(guild_id, 'menu.tip_line_2')}\n"
|
| 643 |
f"? {await self.bot.get_text(guild_id, 'menu.tip_line_3')}"
|
| 644 |
)
|
| 645 |
+
quick = (
|
| 646 |
+
f"- `/music_panel` - {await self.bot.get_text(guild_id, 'menu.quick_music')}\n"
|
| 647 |
+
f"- `/gamehub` - {await self.bot.get_text(guild_id, 'menu.quick_gamehub')}\n"
|
| 648 |
+
f"- `/tournament panel` - {await self.bot.get_text(guild_id, 'menu.quick_tournament')}\n"
|
| 649 |
+
f"- `/economy` - {await self.bot.get_text(guild_id, 'menu.quick_economy')}"
|
| 650 |
+
)
|
| 651 |
+
quick_ai = await self.bot.get_text(guild_id, 'menu.quick_ai')
|
| 652 |
+
updates = (
|
| 653 |
+
f"- `/admin emoji clone` - {await self.bot.get_text(guild_id, 'menu.update_cloneemoji')}\n"
|
| 654 |
+
f"- `/admin shield add_image` - {await self.bot.get_text(guild_id, 'menu.update_shield_image')}\n"
|
| 655 |
+
f"- `/poll create` - {await self.bot.get_text(guild_id, 'menu.update_poll_group')}\n"
|
| 656 |
+
f"- `/economy deposit` - {await self.bot.get_text(guild_id, 'menu.update_economy_group')}\n"
|
| 657 |
+
f"- `/ai execute` - {quick_ai if quick_ai != 'menu.quick_ai' else 'AI admin request'}"
|
| 658 |
+
)
|
| 659 |
+
footer = "Powered by BOT- AI Suite"
|
| 660 |
|
| 661 |
top_divider = panel_divider("cyan")
|
| 662 |
mid_divider = panel_divider("purple")
|
|
|
|
| 682 |
slash_budget = f"{slash_used}/100"
|
| 683 |
stats_grid = quick_stats_grid(
|
| 684 |
[
|
| 685 |
+
("Guilds", str(total_guilds), "G"),
|
| 686 |
+
("Members", f"{total_members:,}", "M"),
|
| 687 |
+
("Latency", latency_ms, "L"),
|
| 688 |
+
("Slash", slash_budget, "S"),
|
| 689 |
],
|
| 690 |
columns=2,
|
| 691 |
)
|
|
|
|
| 697 |
inline=True,
|
| 698 |
)
|
| 699 |
|
| 700 |
+
emoji = _CATEGORY_EMOJIS.get(selected_cog, "C")
|
| 701 |
none_text = await self.bot.get_text(guild_id, "menu.none")
|
| 702 |
embed.add_field(
|
| 703 |
name=f"{emoji} {selected_label}",
|
|
|
|
| 710 |
embed.add_field(name=updates_title, value=updates, inline=False)
|
| 711 |
|
| 712 |
embed.set_footer(text=footer)
|
| 713 |
+
embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}"
|
| 714 |
return embed
|
| 715 |
|
| 716 |
|
|
|
|
| 744 |
|
| 745 |
async def callback(self, interaction: discord.Interaction) -> None:
|
| 746 |
await interaction.response.defer()
|
| 747 |
+
guild_id = interaction.guild.id if interaction.guild else None
|
| 748 |
+
self.label = await self.parent_view.bot.get_text(guild_id, "menu.refresh")
|
| 749 |
await self.parent_view.setup_items()
|
| 750 |
embed = await self.parent_view.build_embed(guild_id, "__all__")
|
| 751 |
await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
|
|
|
|
| 777 |
self.parent_view = parent
|
| 778 |
self.direction = direction
|
| 779 |
label = "Previous" if direction == "prev" else "Next"
|
| 780 |
+
emoji = "⬅️" if direction == "prev" else "➡️"
|
| 781 |
super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary)
|
| 782 |
|
| 783 |
async def callback(self, interaction: discord.Interaction) -> None:
|
|
|
|
| 798 |
def __init__(self, bot: commands.Bot) -> None:
|
| 799 |
self.bot = bot
|
| 800 |
|
| 801 |
+
async def cog_load(self) -> None:
|
| 802 |
+
self.bot.add_view(CommandsMenuView(self.bot))
|
| 803 |
|
| 804 |
+
@commands.hybrid_command(name="menu", description=get_cmd_desc("commands.tools.menu_desc"))
|
| 805 |
async def menu(self, ctx: commands.Context) -> None:
|
| 806 |
"""Display the beautiful command menu."""
|
| 807 |
if ctx.interaction and not ctx.interaction.response.is_done():
|
|
|
|
| 815 |
else:
|
| 816 |
await ctx.reply(embed=embed, view=view)
|
| 817 |
|
| 818 |
+
@commands.hybrid_command(name="start", description=get_cmd_desc("commands.tools.start_desc"), with_app_command=False)
|
| 819 |
async def start_menu(self, ctx: commands.Context) -> None:
|
| 820 |
await self.menu(ctx)
|
| 821 |
|
| 822 |
async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
| 823 |
embed = ImperialMotaz.craft_embed(
|
| 824 |
+
title="⚠️ Command Error | خطأ في الأمر",
|
| 825 |
+
description=f"「 {str(error)[:1000]} 」",
|
| 826 |
color=discord.Color(0x2B2D31),
|
| 827 |
+
footer="🏮 Powered by BOT- AI Suite 🏮",
|
| 828 |
)
|
| 829 |
try:
|
| 830 |
await ctx.reply(embed=embed)
|
bot/cogs/observability.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import difflib
|
| 4 |
-
import json
|
| 5 |
-
import traceback
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from typing import get_args, get_origin
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
import discord
|
| 10 |
from discord.ext import commands
|
| 11 |
|
| 12 |
|
| 13 |
-
class Observability(commands.Cog):
|
| 14 |
def __init__(self, bot: commands.Bot) -> None:
|
| 15 |
self.bot = bot
|
| 16 |
|
|
@@ -46,17 +47,17 @@ class Observability(commands.Cog):
|
|
| 46 |
names = sorted({c.qualified_name for c in self.bot.commands} | {c.name for c in self.bot.commands})
|
| 47 |
return difflib.get_close_matches(name, names, n=5, cutoff=0.4)
|
| 48 |
|
| 49 |
-
async def _safe_reply(self, ctx: commands.Context, message: str) -> None:
|
| 50 |
-
try:
|
| 51 |
-
await ctx.reply(message)
|
| 52 |
-
except (discord.NotFound, discord.InteractionResponded):
|
| 53 |
-
if ctx.channel:
|
| 54 |
-
await ctx.channel.send(message)
|
| 55 |
-
except discord.HTTPException as exc:
|
| 56 |
-
if exc.code not in {10062, 40060}:
|
| 57 |
-
raise
|
| 58 |
-
if ctx.channel:
|
| 59 |
-
await ctx.channel.send(message)
|
| 60 |
|
| 61 |
@commands.Cog.listener()
|
| 62 |
async def on_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
|
@@ -124,8 +125,8 @@ class Observability(commands.Cog):
|
|
| 124 |
embed.add_field(name="Error", value=f"```py\n{tb[-900:]}\n```", inline=False)
|
| 125 |
await channel.send(embed=embed)
|
| 126 |
|
| 127 |
-
@commands.hybrid_command(name="command_fill", description="
|
| 128 |
-
async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None:
|
| 129 |
command = self.bot.get_command(command_name.strip())
|
| 130 |
if not command:
|
| 131 |
close = self._closest_commands(command_name.strip())
|
|
@@ -143,70 +144,70 @@ class Observability(commands.Cog):
|
|
| 143 |
usage = f"{ctx.prefix}{command.qualified_name} " + " ".join(pieces)
|
| 144 |
embed = discord.Embed(title="🧩 Command Fill Helper", color=discord.Color.blurple())
|
| 145 |
embed.add_field(name="Usage", value=f"`{usage.strip()}`", inline=False)
|
| 146 |
-
embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False)
|
| 147 |
-
await ctx.reply(embed=embed)
|
| 148 |
-
|
| 149 |
-
@commands.hybrid_command(name="system_audit", description="
|
| 150 |
-
@commands.has_permissions(manage_guild=True)
|
| 151 |
-
async def system_audit(self, ctx: commands.Context) -> None:
|
| 152 |
-
await self._safe_reply(ctx, "Running system audit...")
|
| 153 |
-
|
| 154 |
-
total_commands = len(self.bot.commands)
|
| 155 |
-
loaded_cogs = len(self.bot.cogs)
|
| 156 |
-
|
| 157 |
-
row = await self.bot.db.fetchone("PRAGMA table_info(guild_config)")
|
| 158 |
-
has_guild_config = bool(row)
|
| 159 |
-
cols = await self.bot.db.fetchall("PRAGMA table_info(guild_config)")
|
| 160 |
-
col_names = {str(r[1]) for r in cols} if cols else set()
|
| 161 |
-
has_banner_col = "custom_banner_url" in col_names
|
| 162 |
-
has_lang_col = "guild_language" in col_names
|
| 163 |
-
|
| 164 |
-
locale_dir = Path("bot/locales")
|
| 165 |
-
locale_files = sorted(p.name for p in locale_dir.glob("*.json")) if locale_dir.exists() else []
|
| 166 |
-
locale_issues = 0
|
| 167 |
-
if locale_files:
|
| 168 |
-
try:
|
| 169 |
-
base = json.loads((locale_dir / "en.json").read_text(encoding="utf-8"))
|
| 170 |
-
def flatten(obj, prefix=""):
|
| 171 |
-
out = {}
|
| 172 |
-
if isinstance(obj, dict):
|
| 173 |
-
for k, v in obj.items():
|
| 174 |
-
key = f"{prefix}.{k}" if prefix else k
|
| 175 |
-
out.update(flatten(v, key))
|
| 176 |
-
else:
|
| 177 |
-
out[prefix] = obj
|
| 178 |
-
return out
|
| 179 |
-
base_keys = set(flatten(base).keys())
|
| 180 |
-
for name in locale_files:
|
| 181 |
-
data = json.loads((locale_dir / name).read_text(encoding="utf-8"))
|
| 182 |
-
keys = set(flatten(data).keys())
|
| 183 |
-
if base_keys - keys:
|
| 184 |
-
locale_issues += 1
|
| 185 |
-
except Exception:
|
| 186 |
-
locale_issues += 1
|
| 187 |
-
|
| 188 |
-
checks = [
|
| 189 |
-
("Commands registered", total_commands >= 50, str(total_commands)),
|
| 190 |
-
("Cogs loaded", loaded_cogs >= 8, str(loaded_cogs)),
|
| 191 |
-
("guild_config table", has_guild_config, "ok" if has_guild_config else "missing"),
|
| 192 |
-
("guild_language column", has_lang_col, "ok" if has_lang_col else "missing"),
|
| 193 |
-
("custom_banner_url column", has_banner_col, "ok" if has_banner_col else "missing"),
|
| 194 |
-
("Locale files", bool(locale_files), f"{len(locale_files)} files"),
|
| 195 |
-
("Locale consistency", locale_issues == 0, "ok" if locale_issues == 0 else f"{locale_issues} issue(s)"),
|
| 196 |
-
]
|
| 197 |
-
|
| 198 |
-
passed = sum(1 for _, ok, _ in checks if ok)
|
| 199 |
-
status = "HEALTHY" if passed == len(checks) else "NEEDS ATTENTION"
|
| 200 |
-
color = discord.Color.green() if passed == len(checks) else discord.Color.orange()
|
| 201 |
-
lines = [f"{'✅' if ok else '⚠️'} {name}: `{detail}`" for name, ok, detail in checks]
|
| 202 |
-
|
| 203 |
-
embed = discord.Embed(
|
| 204 |
-
title=f"System Audit: {status}",
|
| 205 |
-
description="\n".join(lines),
|
| 206 |
-
color=color,
|
| 207 |
-
)
|
| 208 |
-
embed.set_footer(text=f"Passed {passed}/{len(checks)} checks")
|
| 209 |
-
await ctx.reply(embed=embed)
|
| 210 |
|
| 211 |
|
| 212 |
async def setup(bot: commands.Bot) -> None:
|
|
|
|
| 1 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
import difflib
|
| 4 |
+
import json
|
| 5 |
+
import traceback
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import get_args, get_origin
|
| 8 |
+
|
| 9 |
+
from bot.i18n import get_cmd_desc
|
| 10 |
import discord
|
| 11 |
from discord.ext import commands
|
| 12 |
|
| 13 |
|
| 14 |
+
class Observability(commands.Cog):
|
| 15 |
def __init__(self, bot: commands.Bot) -> None:
|
| 16 |
self.bot = bot
|
| 17 |
|
|
|
|
| 47 |
names = sorted({c.qualified_name for c in self.bot.commands} | {c.name for c in self.bot.commands})
|
| 48 |
return difflib.get_close_matches(name, names, n=5, cutoff=0.4)
|
| 49 |
|
| 50 |
+
async def _safe_reply(self, ctx: commands.Context, message: str) -> None:
|
| 51 |
+
try:
|
| 52 |
+
await ctx.reply(message)
|
| 53 |
+
except (discord.NotFound, discord.InteractionResponded):
|
| 54 |
+
if ctx.channel:
|
| 55 |
+
await ctx.channel.send(message)
|
| 56 |
+
except discord.HTTPException as exc:
|
| 57 |
+
if exc.code not in {10062, 40060}:
|
| 58 |
+
raise
|
| 59 |
+
if ctx.channel:
|
| 60 |
+
await ctx.channel.send(message)
|
| 61 |
|
| 62 |
@commands.Cog.listener()
|
| 63 |
async def on_command_error(self, ctx: commands.Context, error: Exception) -> None:
|
|
|
|
| 125 |
embed.add_field(name="Error", value=f"```py\n{tb[-900:]}\n```", inline=False)
|
| 126 |
await channel.send(embed=embed)
|
| 127 |
|
| 128 |
+
@commands.hybrid_command(name="command_fill", description=get_cmd_desc("commands.tools.command_fill_desc"))
|
| 129 |
+
async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None:
|
| 130 |
command = self.bot.get_command(command_name.strip())
|
| 131 |
if not command:
|
| 132 |
close = self._closest_commands(command_name.strip())
|
|
|
|
| 144 |
usage = f"{ctx.prefix}{command.qualified_name} " + " ".join(pieces)
|
| 145 |
embed = discord.Embed(title="🧩 Command Fill Helper", color=discord.Color.blurple())
|
| 146 |
embed.add_field(name="Usage", value=f"`{usage.strip()}`", inline=False)
|
| 147 |
+
embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False)
|
| 148 |
+
await ctx.reply(embed=embed)
|
| 149 |
+
|
| 150 |
+
@commands.hybrid_command(name="system_audit", description=get_cmd_desc("commands.tools.system_audit_desc"))
|
| 151 |
+
@commands.has_permissions(manage_guild=True)
|
| 152 |
+
async def system_audit(self, ctx: commands.Context) -> None:
|
| 153 |
+
await self._safe_reply(ctx, "Running system audit...")
|
| 154 |
+
|
| 155 |
+
total_commands = len(self.bot.commands)
|
| 156 |
+
loaded_cogs = len(self.bot.cogs)
|
| 157 |
+
|
| 158 |
+
row = await self.bot.db.fetchone("PRAGMA table_info(guild_config)")
|
| 159 |
+
has_guild_config = bool(row)
|
| 160 |
+
cols = await self.bot.db.fetchall("PRAGMA table_info(guild_config)")
|
| 161 |
+
col_names = {str(r[1]) for r in cols} if cols else set()
|
| 162 |
+
has_banner_col = "custom_banner_url" in col_names
|
| 163 |
+
has_lang_col = "guild_language" in col_names
|
| 164 |
+
|
| 165 |
+
locale_dir = Path("bot/locales")
|
| 166 |
+
locale_files = sorted(p.name for p in locale_dir.glob("*.json")) if locale_dir.exists() else []
|
| 167 |
+
locale_issues = 0
|
| 168 |
+
if locale_files:
|
| 169 |
+
try:
|
| 170 |
+
base = json.loads((locale_dir / "en.json").read_text(encoding="utf-8"))
|
| 171 |
+
def flatten(obj, prefix=""):
|
| 172 |
+
out = {}
|
| 173 |
+
if isinstance(obj, dict):
|
| 174 |
+
for k, v in obj.items():
|
| 175 |
+
key = f"{prefix}.{k}" if prefix else k
|
| 176 |
+
out.update(flatten(v, key))
|
| 177 |
+
else:
|
| 178 |
+
out[prefix] = obj
|
| 179 |
+
return out
|
| 180 |
+
base_keys = set(flatten(base).keys())
|
| 181 |
+
for name in locale_files:
|
| 182 |
+
data = json.loads((locale_dir / name).read_text(encoding="utf-8"))
|
| 183 |
+
keys = set(flatten(data).keys())
|
| 184 |
+
if base_keys - keys:
|
| 185 |
+
locale_issues += 1
|
| 186 |
+
except Exception:
|
| 187 |
+
locale_issues += 1
|
| 188 |
+
|
| 189 |
+
checks = [
|
| 190 |
+
("Commands registered", total_commands >= 50, str(total_commands)),
|
| 191 |
+
("Cogs loaded", loaded_cogs >= 8, str(loaded_cogs)),
|
| 192 |
+
("guild_config table", has_guild_config, "ok" if has_guild_config else "missing"),
|
| 193 |
+
("guild_language column", has_lang_col, "ok" if has_lang_col else "missing"),
|
| 194 |
+
("custom_banner_url column", has_banner_col, "ok" if has_banner_col else "missing"),
|
| 195 |
+
("Locale files", bool(locale_files), f"{len(locale_files)} files"),
|
| 196 |
+
("Locale consistency", locale_issues == 0, "ok" if locale_issues == 0 else f"{locale_issues} issue(s)"),
|
| 197 |
+
]
|
| 198 |
+
|
| 199 |
+
passed = sum(1 for _, ok, _ in checks if ok)
|
| 200 |
+
status = "HEALTHY" if passed == len(checks) else "NEEDS ATTENTION"
|
| 201 |
+
color = discord.Color.green() if passed == len(checks) else discord.Color.orange()
|
| 202 |
+
lines = [f"{'✅' if ok else '⚠️'} {name}: `{detail}`" for name, ok, detail in checks]
|
| 203 |
+
|
| 204 |
+
embed = discord.Embed(
|
| 205 |
+
title=f"System Audit: {status}",
|
| 206 |
+
description="\n".join(lines),
|
| 207 |
+
color=color,
|
| 208 |
+
)
|
| 209 |
+
embed.set_footer(text=f"Passed {passed}/{len(checks)} checks")
|
| 210 |
+
await ctx.reply(embed=embed)
|
| 211 |
|
| 212 |
|
| 213 |
async def setup(bot: commands.Bot) -> None:
|
bot/cogs/server_manager.py
CHANGED
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
| 3 |
import discord
|
| 4 |
from discord.ext import commands
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
| 7 |
class ServerManager(commands.Cog):
|
| 8 |
def __init__(self, bot: commands.Bot) -> None:
|
|
@@ -30,10 +32,10 @@ class ServerManager(commands.Cog):
|
|
| 30 |
await channel.edit(category=category)
|
| 31 |
return channel
|
| 32 |
|
| 33 |
-
@commands.hybrid_command(name="setupserver", description="
|
| 34 |
-
@commands.has_permissions(administrator=True)
|
| 35 |
-
async def setupserver_cmd(self, ctx: commands.Context) -> None:
|
| 36 |
-
guild = ctx.guild
|
| 37 |
|
| 38 |
category_general = await self.ensure_category(guild, "📌・الإدارة")
|
| 39 |
category_community = await self.ensure_category(guild, "💬・المجتمع")
|
|
@@ -98,7 +100,7 @@ class ServerManager(commands.Cog):
|
|
| 98 |
embed.add_field(name="Core", value=f"{welcome.mention} | {logs.mention} | {daily.mention} | {suggestions.mention}", inline=False)
|
| 99 |
await ctx.reply(embed=embed)
|
| 100 |
|
| 101 |
-
@commands.hybrid_command(name="organizechannels", description="
|
| 102 |
@commands.has_permissions(administrator=True)
|
| 103 |
async def organize_channels(self, ctx: commands.Context) -> None:
|
| 104 |
guild = ctx.guild
|
|
|
|
| 3 |
import discord
|
| 4 |
from discord.ext import commands
|
| 5 |
|
| 6 |
+
from bot.i18n import get_cmd_desc
|
| 7 |
+
|
| 8 |
|
| 9 |
class ServerManager(commands.Cog):
|
| 10 |
def __init__(self, bot: commands.Bot) -> None:
|
|
|
|
| 32 |
await channel.edit(category=category)
|
| 33 |
return channel
|
| 34 |
|
| 35 |
+
@commands.hybrid_command(name="setupserver", description=get_cmd_desc("commands.tools.setupserver_desc"))
|
| 36 |
+
@commands.has_permissions(administrator=True)
|
| 37 |
+
async def setupserver_cmd(self, ctx: commands.Context) -> None:
|
| 38 |
+
guild = ctx.guild
|
| 39 |
|
| 40 |
category_general = await self.ensure_category(guild, "📌・الإدارة")
|
| 41 |
category_community = await self.ensure_category(guild, "💬・المجتمع")
|
|
|
|
| 100 |
embed.add_field(name="Core", value=f"{welcome.mention} | {logs.mention} | {daily.mention} | {suggestions.mention}", inline=False)
|
| 101 |
await ctx.reply(embed=embed)
|
| 102 |
|
| 103 |
+
@commands.hybrid_command(name="organizechannels", description=get_cmd_desc("commands.tools.organizechannels_desc"))
|
| 104 |
@commands.has_permissions(administrator=True)
|
| 105 |
async def organize_channels(self, ctx: commands.Context) -> None:
|
| 106 |
guild = ctx.guild
|
bot/cogs/utility.py
CHANGED
|
@@ -1,358 +1,556 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import asyncio
|
| 4 |
-
import time
|
| 5 |
-
|
| 6 |
-
import discord
|
| 7 |
-
import psutil
|
| 8 |
-
from discord.ext import commands
|
| 9 |
-
|
| 10 |
-
from bot.theme import NEON_CYAN, NEON_PINK, NEON_PURPLE, progress_bar, add_banner_to_embed
|
| 11 |
-
from bot.
|
| 12 |
-
from bot.emojis import ui
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
self.
|
| 32 |
-
self.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
await self.
|
| 44 |
-
self.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
self.
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
self.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
await
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
self.
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
self.
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
self.
|
| 156 |
-
self.
|
| 157 |
-
self.
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
self.
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
self.bot.add_view(
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.
|
| 208 |
-
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.
|
| 209 |
-
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.
|
| 210 |
-
embed.add_field(name="
|
| 211 |
-
embed.add_field(name="
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
embed.
|
| 225 |
-
embed.add_field(name=await self.bot.tr(guild_id, "utility.userinfo.
|
| 226 |
-
embed.add_field(name=await self.bot.tr(guild_id, "utility.userinfo.
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
guild_id,
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
embed.add_field(name="
|
| 238 |
-
embed.add_field(name="XP
|
| 239 |
-
embed.add_field(name="XP
|
| 240 |
-
|
| 241 |
-
embed.
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
embed.add_field(name=await self.bot.tr(guild_id, "botstats.
|
| 260 |
-
embed.add_field(name=await self.bot.tr(guild_id, "botstats.
|
| 261 |
-
embed.add_field(name=await self.bot.tr(guild_id, "botstats.
|
| 262 |
-
embed.add_field(name=await self.bot.tr(guild_id, "botstats.
|
| 263 |
-
embed.add_field(name=await self.bot.tr(guild_id, "botstats.
|
| 264 |
-
embed.
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
view
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
view.
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
ctx.
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
import discord
|
| 7 |
+
import psutil
|
| 8 |
+
from discord.ext import commands
|
| 9 |
+
|
| 10 |
+
from bot.theme import NEON_CYAN, NEON_PINK, NEON_PURPLE, progress_bar, add_banner_to_embed
|
| 11 |
+
from bot.i18n import get_cmd_desc
|
| 12 |
+
from bot.emojis import ui
|
| 13 |
+
from bot.emojis import ui
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 17 |
+
# AUTO REFRESH MIXIN - ميزة التحديث التلقائي
|
| 18 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 19 |
+
|
| 20 |
+
class AutoRefreshMixin:
|
| 21 |
+
"""Mixin class for auto-refresh functionality."""
|
| 22 |
+
|
| 23 |
+
_refresh_interval: int = 4
|
| 24 |
+
_refresh_task: asyncio.Task = None
|
| 25 |
+
_message: discord.Message = None
|
| 26 |
+
_stopped: bool = False
|
| 27 |
+
_consecutive_failures: int = 0
|
| 28 |
+
|
| 29 |
+
async def start_auto_refresh(self, message: discord.Message) -> None:
|
| 30 |
+
"""Start auto-refresh task."""
|
| 31 |
+
self._message = message
|
| 32 |
+
self._stopped = False
|
| 33 |
+
self._refresh_task = asyncio.create_task(self._auto_refresh_loop())
|
| 34 |
+
|
| 35 |
+
async def _auto_refresh_loop(self) -> None:
|
| 36 |
+
"""Auto refresh loop."""
|
| 37 |
+
while not self._stopped:
|
| 38 |
+
try:
|
| 39 |
+
await asyncio.sleep(self._refresh_interval)
|
| 40 |
+
if self._stopped or not self._message:
|
| 41 |
+
break
|
| 42 |
+
# Build new embed
|
| 43 |
+
embed = await self._build_refresh_embed()
|
| 44 |
+
await self._message.edit(embed=embed, view=self)
|
| 45 |
+
self._consecutive_failures = 0
|
| 46 |
+
except discord.NotFound:
|
| 47 |
+
break
|
| 48 |
+
except discord.HTTPException:
|
| 49 |
+
self._consecutive_failures += 1
|
| 50 |
+
if self._consecutive_failures >= 5:
|
| 51 |
+
break
|
| 52 |
+
await asyncio.sleep(2)
|
| 53 |
+
continue
|
| 54 |
+
except asyncio.CancelledError:
|
| 55 |
+
break
|
| 56 |
+
except Exception:
|
| 57 |
+
self._consecutive_failures += 1
|
| 58 |
+
if self._consecutive_failures >= 5:
|
| 59 |
+
break
|
| 60 |
+
await asyncio.sleep(2)
|
| 61 |
+
|
| 62 |
+
def stop_refresh(self) -> None:
|
| 63 |
+
"""Stop auto-refresh."""
|
| 64 |
+
self._stopped = True
|
| 65 |
+
if self._refresh_task and not self._refresh_task.done():
|
| 66 |
+
self._refresh_task.cancel()
|
| 67 |
+
|
| 68 |
+
async def _build_refresh_embed(self) -> discord.Embed:
|
| 69 |
+
"""Override this method to build refresh embed."""
|
| 70 |
+
raise NotImplementedError
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class ServerInfoView(discord.ui.View, AutoRefreshMixin):
|
| 74 |
+
def __init__(self, cog: "Utility") -> None:
|
| 75 |
+
discord.ui.View.__init__(self, timeout=None)
|
| 76 |
+
AutoRefreshMixin.__init__(self)
|
| 77 |
+
self.cog = cog
|
| 78 |
+
|
| 79 |
+
@discord.ui.button(label="Refresh", emoji=ui("refresh"), style=discord.ButtonStyle.blurple, custom_id="utility:serverinfo:refresh")
|
| 80 |
+
async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 81 |
+
if not interaction.guild:
|
| 82 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 83 |
+
return
|
| 84 |
+
self._message = interaction.message
|
| 85 |
+
self.refresh.label = await self.cog.bot.tr(interaction.guild.id, "utility.refresh")
|
| 86 |
+
embed = await self.cog.build_serverinfo_embed(interaction.guild)
|
| 87 |
+
try:
|
| 88 |
+
await interaction.response.edit_message(embed=embed, view=self)
|
| 89 |
+
except (discord.NotFound, discord.InteractionResponded):
|
| 90 |
+
if interaction.message:
|
| 91 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 92 |
+
except discord.HTTPException:
|
| 93 |
+
if interaction.message:
|
| 94 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 95 |
+
|
| 96 |
+
async def _build_refresh_embed(self) -> discord.Embed:
|
| 97 |
+
if self._message and self._message.guild:
|
| 98 |
+
return await self.cog.build_serverinfo_embed(self._message.guild)
|
| 99 |
+
return discord.Embed(title="Server Info", description="Server panel is ready.")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class UserInfoView(discord.ui.View):
|
| 103 |
+
def __init__(self, cog: "Utility", member_id: int) -> None:
|
| 104 |
+
super().__init__(timeout=None)
|
| 105 |
+
self.cog = cog
|
| 106 |
+
self.member_id = member_id
|
| 107 |
+
|
| 108 |
+
@discord.ui.button(label="Refresh", emoji=ui("refresh"), style=discord.ButtonStyle.blurple)
|
| 109 |
+
async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 110 |
+
if not interaction.guild:
|
| 111 |
+
await interaction.response.send_message("Server only.", ephemeral=True)
|
| 112 |
+
return
|
| 113 |
+
member = interaction.guild.get_member(self.member_id)
|
| 114 |
+
if not member:
|
| 115 |
+
await interaction.response.send_message("Member not found.", ephemeral=True)
|
| 116 |
+
return
|
| 117 |
+
self.refresh.label = await self.cog.bot.tr(interaction.guild.id, "utility.refresh")
|
| 118 |
+
embed = await self.cog.build_userinfo_embed(interaction.guild.id, member)
|
| 119 |
+
await interaction.response.edit_message(embed=embed, view=self)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class BotStatsView(discord.ui.View, AutoRefreshMixin):
|
| 125 |
+
def __init__(self, cog: "Utility", guild_id: int | None = None) -> None:
|
| 126 |
+
discord.ui.View.__init__(self, timeout=None)
|
| 127 |
+
AutoRefreshMixin.__init__(self)
|
| 128 |
+
self.cog = cog
|
| 129 |
+
self.guild_id = guild_id
|
| 130 |
+
|
| 131 |
+
@discord.ui.button(label="Refresh", emoji="⛩️", style=discord.ButtonStyle.blurple, custom_id="utility:botstats:refresh")
|
| 132 |
+
async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 133 |
+
guild_id = interaction.guild.id if interaction.guild else None
|
| 134 |
+
self.refresh.label = await self.cog.bot.tr(guild_id, "utility.refresh")
|
| 135 |
+
self.guild_id = guild_id
|
| 136 |
+
embed = await self._build_refresh_embed()
|
| 137 |
+
try:
|
| 138 |
+
await interaction.response.edit_message(embed=embed, view=self)
|
| 139 |
+
except (discord.NotFound, discord.InteractionResponded):
|
| 140 |
+
if interaction.message:
|
| 141 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 142 |
+
except discord.HTTPException:
|
| 143 |
+
if interaction.message:
|
| 144 |
+
await interaction.message.edit(embed=embed, view=self)
|
| 145 |
+
|
| 146 |
+
async def _build_refresh_embed(self) -> discord.Embed:
|
| 147 |
+
return await self.cog.build_botstats_embed(self.guild_id)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class UtilityPollVoteView(discord.ui.View):
|
| 153 |
+
def __init__(self, *, question: str, options: list[str], author_id: int) -> None:
|
| 154 |
+
super().__init__(timeout=None)
|
| 155 |
+
self.question = question
|
| 156 |
+
self.options = options
|
| 157 |
+
self.author_id = author_id
|
| 158 |
+
self.voters: dict[int, int] = {}
|
| 159 |
+
for idx, option in enumerate(options):
|
| 160 |
+
btn = discord.ui.Button(label=option[:80], style=discord.ButtonStyle.secondary, custom_id=f"poll:{idx}")
|
| 161 |
+
|
| 162 |
+
async def _callback(interaction: discord.Interaction, i: int = idx) -> None:
|
| 163 |
+
self.voters[interaction.user.id] = i
|
| 164 |
+
await interaction.response.send_message(f"✅ Vote saved: **{self.options[i]}**", ephemeral=True)
|
| 165 |
+
if self.message:
|
| 166 |
+
await self.message.edit(embed=self._build_embed(), view=self)
|
| 167 |
+
|
| 168 |
+
btn.callback = _callback
|
| 169 |
+
self.add_item(btn)
|
| 170 |
+
self.message: discord.Message | None = None
|
| 171 |
+
|
| 172 |
+
def _build_embed(self) -> discord.Embed:
|
| 173 |
+
total = len(self.voters)
|
| 174 |
+
embed = discord.Embed(title="🗳️ Community Poll", description=f"### {self.question}", color=NEON_CYAN)
|
| 175 |
+
for idx, option in enumerate(self.options):
|
| 176 |
+
count = sum(1 for choice in self.voters.values() if choice == idx)
|
| 177 |
+
ratio = 0 if total == 0 else int((count / total) * 100)
|
| 178 |
+
bar = "█" * max(1, ratio // 10) if total else "-"
|
| 179 |
+
embed.add_field(name=option, value=f"{count} votes • {ratio}%\n`{bar}`", inline=False)
|
| 180 |
+
embed.set_footer(text=f"Total voters: {total} • Poll by <@{self.author_id}> • You can change your vote any time")
|
| 181 |
+
return embed
|
| 182 |
+
|
| 183 |
+
class Utility(commands.Cog):
|
| 184 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 185 |
+
self.bot = bot
|
| 186 |
+
self.started_at = time.time()
|
| 187 |
+
|
| 188 |
+
async def cog_load(self) -> None:
|
| 189 |
+
# Register persistent views for static panels.
|
| 190 |
+
self.bot.add_view(ServerInfoView(self))
|
| 191 |
+
self.bot.add_view(BotStatsView(self))
|
| 192 |
+
|
| 193 |
+
async def build_serverinfo_embed(self, guild: discord.Guild) -> discord.Embed:
|
| 194 |
+
text_channels = len(guild.text_channels)
|
| 195 |
+
voice_channels = len(guild.voice_channels)
|
| 196 |
+
categories = len(guild.categories)
|
| 197 |
+
bots = sum(1 for m in guild.members if m.bot)
|
| 198 |
+
humans = (guild.member_count or 0) - bots
|
| 199 |
+
owner = guild.owner.mention if guild.owner else "Unknown"
|
| 200 |
+
created = f"<t:{int(guild.created_at.timestamp())}:F>\n<t:{int(guild.created_at.timestamp())}:R>"
|
| 201 |
+
|
| 202 |
+
embed = discord.Embed(
|
| 203 |
+
title=f"╔════╗ {await self.bot.tr(guild.id, 'utility.serverinfo.title')} ╗════",
|
| 204 |
+
description=f"**{guild.name}**\nOwner: {owner}",
|
| 205 |
+
color=NEON_CYAN,
|
| 206 |
+
)
|
| 207 |
+
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.members"), value=f"Total: {guild.member_count}\nHumans: {humans}\nBots: {bots}", inline=True)
|
| 208 |
+
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.channels"), value=f"Total: {len(guild.channels)}\nText: {text_channels}\nVoice: {voice_channels}\nCategories: {categories}", inline=True)
|
| 209 |
+
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.roles"), value=str(len(guild.roles)), inline=True)
|
| 210 |
+
embed.add_field(name=await self.bot.tr(guild.id, "utility.serverinfo.boost"), value=f"Tier {guild.premium_tier}\nBoosts: {guild.premium_subscription_count or 0}", inline=True)
|
| 211 |
+
embed.add_field(name="Created", value=created, inline=False)
|
| 212 |
+
embed.add_field(name="Server ID", value=str(guild.id), inline=False)
|
| 213 |
+
|
| 214 |
+
# Add server banner if available
|
| 215 |
+
if guild.banner:
|
| 216 |
+
embed.set_image(url=guild.banner.url)
|
| 217 |
+
|
| 218 |
+
if guild.icon:
|
| 219 |
+
embed.set_thumbnail(url=guild.icon.url)
|
| 220 |
+
embed.timestamp = discord.utils.utcnow()
|
| 221 |
+
return embed
|
| 222 |
+
|
| 223 |
+
async def build_userinfo_embed(self, guild_id: int | None, member: discord.Member) -> discord.Embed:
|
| 224 |
+
embed = discord.Embed(title=await self.bot.tr(guild_id, "utility.userinfo.title"), color=NEON_PINK)
|
| 225 |
+
embed.add_field(name=await self.bot.tr(guild_id, "utility.userinfo.id"), value=str(member.id), inline=False)
|
| 226 |
+
embed.add_field(name=await self.bot.tr(guild_id, "utility.userinfo.joined"), value=str(member.joined_at), inline=False)
|
| 227 |
+
embed.add_field(name=await self.bot.tr(guild_id, "utility.userinfo.created"), value=str(member.created_at), inline=False)
|
| 228 |
+
if guild_id:
|
| 229 |
+
row = await self.bot.db.fetchone(
|
| 230 |
+
"SELECT xp, level FROM user_xp WHERE guild_id = ? AND user_id = ?",
|
| 231 |
+
guild_id,
|
| 232 |
+
member.id,
|
| 233 |
+
)
|
| 234 |
+
xp, level = row if row else (0, 1)
|
| 235 |
+
target = max(1, level * 150)
|
| 236 |
+
remaining = max(0, target - xp)
|
| 237 |
+
embed.add_field(name="Level", value=str(level), inline=True)
|
| 238 |
+
embed.add_field(name="XP", value=f"{xp}/{target}", inline=True)
|
| 239 |
+
embed.add_field(name="XP Progress", value=progress_bar(xp, target), inline=False)
|
| 240 |
+
embed.add_field(name="XP to next level", value=str(remaining), inline=True)
|
| 241 |
+
embed.set_thumbnail(url=member.display_avatar.url)
|
| 242 |
+
embed.timestamp = discord.utils.utcnow()
|
| 243 |
+
return embed
|
| 244 |
+
|
| 245 |
+
async def build_botstats_embed(self, guild_id: int | None) -> discord.Embed:
|
| 246 |
+
process = psutil.Process()
|
| 247 |
+
mem = process.memory_info().rss / (1024 * 1024)
|
| 248 |
+
uptime_seconds = int(time.time() - self.started_at)
|
| 249 |
+
days, rem = divmod(uptime_seconds, 86400)
|
| 250 |
+
hours, rem = divmod(rem, 3600)
|
| 251 |
+
minutes, seconds = divmod(rem, 60)
|
| 252 |
+
uptime = f"{days}d {hours}h {minutes}m {seconds}s"
|
| 253 |
+
latency_ms = round(self.bot.latency * 1000)
|
| 254 |
+
|
| 255 |
+
embed = discord.Embed(
|
| 256 |
+
title=f"BOT- AI System\n╔════╗ {await self.bot.tr(guild_id, 'botstats.title')} ╗════",
|
| 257 |
+
color=NEON_PURPLE,
|
| 258 |
+
)
|
| 259 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.servers"), value=str(len(self.bot.guilds)), inline=True)
|
| 260 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.users"), value=str(len(self.bot.users)), inline=True)
|
| 261 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.latency"), value=f"{latency_ms}ms", inline=True)
|
| 262 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.cpu"), value=f"{psutil.cpu_percent()}%", inline=True)
|
| 263 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.ram"), value=f"{mem:.1f}MB", inline=True)
|
| 264 |
+
embed.add_field(name=await self.bot.tr(guild_id, "botstats.uptime"), value=uptime, inline=True)
|
| 265 |
+
embed.timestamp = discord.utils.utcnow()
|
| 266 |
+
return embed
|
| 267 |
+
|
| 268 |
+
@commands.hybrid_command(name="serverinfo", description=get_cmd_desc("commands.tools.serverinfo_desc"))
|
| 269 |
+
async def serverinfo(self, ctx: commands.Context) -> None:
|
| 270 |
+
g = ctx.guild
|
| 271 |
+
if not g:
|
| 272 |
+
await ctx.reply("Server only.")
|
| 273 |
+
return
|
| 274 |
+
embed = await self.build_serverinfo_embed(g)
|
| 275 |
+
view = ServerInfoView(self)
|
| 276 |
+
view.refresh.label = await self.bot.tr(g.id, "utility.refresh")
|
| 277 |
+
msg = await ctx.reply(embed=embed, view=view)
|
| 278 |
+
if msg is None and ctx.interaction:
|
| 279 |
+
try:
|
| 280 |
+
msg = await ctx.interaction.original_response()
|
| 281 |
+
except Exception:
|
| 282 |
+
msg = None
|
| 283 |
+
view._message = msg
|
| 284 |
+
if msg:
|
| 285 |
+
await view.start_auto_refresh(msg)
|
| 286 |
+
|
| 287 |
+
@commands.hybrid_command(name="userinfo", description=get_cmd_desc("commands.tools.userinfo_desc"))
|
| 288 |
+
async def userinfo(self, ctx: commands.Context, member: discord.Member | None = None) -> None:
|
| 289 |
+
member = member or ctx.author
|
| 290 |
+
embed = await self.build_userinfo_embed(ctx.guild.id if ctx.guild else None, member)
|
| 291 |
+
view = UserInfoView(self, member.id)
|
| 292 |
+
if ctx.guild:
|
| 293 |
+
view.refresh.label = await self.bot.tr(ctx.guild.id, "utility.refresh")
|
| 294 |
+
await ctx.reply(embed=embed, view=view)
|
| 295 |
+
|
| 296 |
+
@commands.hybrid_command(name="poll_legacy", description=get_cmd_desc("commands.tools.poll_legacy_desc"), hidden=True, with_app_command=False)
|
| 297 |
+
async def poll(self, ctx: commands.Context, question: str, options: str = "") -> None:
|
| 298 |
+
parts = [p.strip() for p in options.split("|") if p.strip()]
|
| 299 |
+
if not parts:
|
| 300 |
+
parts = ["Yes", "No", "Maybe", "Skip", "Later"]
|
| 301 |
+
if len(parts) == 1:
|
| 302 |
+
parts.extend(["Option 2", "Option 3", "Option 4"])
|
| 303 |
+
elif len(parts) == 2:
|
| 304 |
+
parts.extend(["Option 3", "Option 4", "Option 5"])
|
| 305 |
+
parts = parts[:10]
|
| 306 |
+
row = await self.bot.db.fetchone("SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", ctx.guild.id) if ctx.guild else None
|
| 307 |
+
channel = ctx.guild.get_channel(row[0]) if (ctx.guild and row and row[0]) else ctx.channel
|
| 308 |
+
view = PollVoteView(question=question, options=parts, author_id=ctx.author.id)
|
| 309 |
+
msg = await channel.send(embed=view._build_embed(), view=view)
|
| 310 |
+
view.message = msg
|
| 311 |
+
await ctx.reply(f"✅ Poll published in {channel.mention} with {len(parts)} options by {ctx.author.mention}.")
|
| 312 |
+
|
| 313 |
+
@commands.hybrid_command(name="remind", description=get_cmd_desc("commands.tools.remind_desc"))
|
| 314 |
+
async def remind(self, ctx: commands.Context, seconds: int, *, text: str) -> None:
|
| 315 |
+
seconds = max(5, min(seconds, 604800))
|
| 316 |
+
due = int(time.time()) + seconds
|
| 317 |
+
await self.bot.db.execute(
|
| 318 |
+
"INSERT INTO reminders(user_id, channel_id, message, due_unix) VALUES (?, ?, ?, ?)",
|
| 319 |
+
ctx.author.id,
|
| 320 |
+
ctx.channel.id,
|
| 321 |
+
text,
|
| 322 |
+
due,
|
| 323 |
+
)
|
| 324 |
+
await ctx.reply(f"⏰ Reminder set in {seconds} seconds.")
|
| 325 |
+
|
| 326 |
+
@commands.Cog.listener()
|
| 327 |
+
async def on_message(self, message: discord.Message) -> None:
|
| 328 |
+
if message.author.bot:
|
| 329 |
+
return
|
| 330 |
+
now = int(time.time())
|
| 331 |
+
due_rows = await self.bot.db.fetchall(
|
| 332 |
+
"SELECT id, user_id, channel_id, message FROM reminders WHERE due_unix <= ? LIMIT 20",
|
| 333 |
+
now,
|
| 334 |
+
)
|
| 335 |
+
for rid, user_id, channel_id, msg in due_rows:
|
| 336 |
+
channel = self.bot.get_channel(channel_id)
|
| 337 |
+
if channel:
|
| 338 |
+
await channel.send(f"⏰ <@{user_id}> reminder: {msg}")
|
| 339 |
+
await self.bot.db.execute("DELETE FROM reminders WHERE id = ?", rid)
|
| 340 |
+
|
| 341 |
+
@commands.hybrid_command(name="search", description=get_cmd_desc("commands.tools.search_desc"))
|
| 342 |
+
@discord.app_commands.describe(query="Search query | بحث")
|
| 343 |
+
async def search(self, ctx: commands.Context, query: str) -> None:
|
| 344 |
+
"""Search YouTube and show results with preview."""
|
| 345 |
+
if ctx.interaction and not ctx.interaction.response.is_done():
|
| 346 |
+
await ctx.interaction.response.defer()
|
| 347 |
+
|
| 348 |
+
loading_embed = discord.Embed(
|
| 349 |
+
title="🔎 Searching YouTube...",
|
| 350 |
+
description=f"🔍 **{query}**\n⏳ Please wait...",
|
| 351 |
+
color=discord.Color.orange(),
|
| 352 |
+
)
|
| 353 |
+
loading_msg = await ctx.send(embed=loading_embed)
|
| 354 |
+
|
| 355 |
+
results = await _yt_search(query, max_results=25)
|
| 356 |
+
|
| 357 |
+
if not results:
|
| 358 |
+
error_embed = discord.Embed(
|
| 359 |
+
title="❌ No Results Found",
|
| 360 |
+
description=f"No videos found for **{query}**.\nTry a different search term.",
|
| 361 |
+
color=discord.Color.red(),
|
| 362 |
+
)
|
| 363 |
+
try:
|
| 364 |
+
await loading_msg.edit(embed=error_embed, view=None)
|
| 365 |
+
except discord.NotFound:
|
| 366 |
+
await ctx.send(embed=error_embed)
|
| 367 |
+
return
|
| 368 |
+
|
| 369 |
+
view = SearchView(self, results, query)
|
| 370 |
+
first = results[0]
|
| 371 |
+
preview_embed = discord.Embed(
|
| 372 |
+
title=f"📺 {first.get('title', 'Untitled')}",
|
| 373 |
+
description=(
|
| 374 |
+
f"**📺 Channel:** {first.get('channel', 'Unknown')}\n"
|
| 375 |
+
f"**👁️ Views:** {first.get('views', 'N/A')}\n"
|
| 376 |
+
f"**⏱️ Duration:** {first.get('duration', 'N/A')}\n\n"
|
| 377 |
+
f"🔗 **[Watch Video]({first.get('url', '')})**\n\n"
|
| 378 |
+
f"📋 **{len(results)}** results found — select one below:"
|
| 379 |
+
),
|
| 380 |
+
url=first.get("url", ""),
|
| 381 |
+
color=discord.Color.red(),
|
| 382 |
+
)
|
| 383 |
+
thumbnail = first.get("thumbnail")
|
| 384 |
+
if thumbnail:
|
| 385 |
+
preview_embed.set_image(url=thumbnail)
|
| 386 |
+
preview_embed.set_footer(text=f"YouTube Search | صفحة 1 من {min(len(results), 25)}")
|
| 387 |
+
|
| 388 |
+
try:
|
| 389 |
+
await loading_msg.edit(embed=preview_embed, view=view)
|
| 390 |
+
except discord.NotFound:
|
| 391 |
+
await ctx.send(embed=preview_embed, view=view)
|
| 392 |
+
|
| 393 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 394 |
+
# YOUTUBE SEARCH COMMAND
|
| 395 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 396 |
+
|
| 397 |
+
_YT_AUTOCOMPLETE_CACHE: dict[str, list[str]] = {}
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
class SearchSelect(discord.ui.Select):
|
| 401 |
+
"""Select a YouTube video from search results."""
|
| 402 |
+
|
| 403 |
+
def __init__(self, cog, results: list, query: str) -> None:
|
| 404 |
+
self.cog = cog
|
| 405 |
+
self.results = results
|
| 406 |
+
self.query = query
|
| 407 |
+
options = []
|
| 408 |
+
for i, vid in enumerate(results[:25]):
|
| 409 |
+
title = vid.get("title", "Untitled")[:100]
|
| 410 |
+
channel = vid.get("channel", "")[:50]
|
| 411 |
+
label = f"{title}"
|
| 412 |
+
desc = f"📺 {channel}" if channel else None
|
| 413 |
+
options.append(discord.SelectOption(label=label, description=desc, value=str(i), emoji="▶️"))
|
| 414 |
+
super().__init__(
|
| 415 |
+
placeholder="🔎 Select a video to preview...",
|
| 416 |
+
options=options,
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
async def callback(self, interaction: discord.Interaction) -> None:
|
| 420 |
+
idx = int(self.values[0])
|
| 421 |
+
vid = self.results[idx]
|
| 422 |
+
embed = discord.Embed(
|
| 423 |
+
title=f"📺 {vid.get('title', 'Untitled')}",
|
| 424 |
+
description=f"**Channel:** {vid.get('channel', 'Unknown')}\n**Views:** {vid.get('views', 'N/A')}\n**Duration:** {vid.get('duration', 'N/A')}\n\n🔗 **Link:** {vid.get('url', '')}",
|
| 425 |
+
url=vid.get("url", ""),
|
| 426 |
+
color=discord.Color.red(),
|
| 427 |
+
)
|
| 428 |
+
thumbnail = vid.get("thumbnail")
|
| 429 |
+
if thumbnail:
|
| 430 |
+
embed.set_image(url=thumbnail)
|
| 431 |
+
embed.set_footer(text="YouTube Search | ابحث في يوتيوب")
|
| 432 |
+
await interaction.response.send_message(embed=embed)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
class SearchView(discord.ui.View):
|
| 436 |
+
def __init__(self, cog, results: list, query: str) -> None:
|
| 437 |
+
super().__init__(timeout=120)
|
| 438 |
+
self.add_item(SearchSelect(cog, results, query))
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
class YouTubeAutocomplete(discord.app_commands.Transformer):
|
| 442 |
+
"""Transformer that provides YouTube search suggestions."""
|
| 443 |
+
|
| 444 |
+
async def transform(self, interaction: discord.Interaction, value: str) -> str:
|
| 445 |
+
return value
|
| 446 |
+
|
| 447 |
+
async def autocomplete(self, interaction: discord.Interaction, current: str) -> list[discord.app_commands.Choice[str]]:
|
| 448 |
+
if not current or len(current) < 2:
|
| 449 |
+
return [discord.app_commands.Choice(name="🔎 Type to search YouTube...", value="")]
|
| 450 |
+
cache_key = current.lower()[:50]
|
| 451 |
+
if cache_key in _YT_AUTOCOMPLETE_CACHE:
|
| 452 |
+
suggestions = _YT_AUTOCOMPLETE_CACHE[cache_key]
|
| 453 |
+
else:
|
| 454 |
+
suggestions = await _yt_autocomplete_fetch(cache_key)
|
| 455 |
+
_YT_AUTOCOMPLETE_CACHE[cache_key] = suggestions
|
| 456 |
+
choices = []
|
| 457 |
+
for s in suggestions[:25]:
|
| 458 |
+
name = s[:100]
|
| 459 |
+
if len(s) > 100:
|
| 460 |
+
name = s[:97] + "..."
|
| 461 |
+
choices.append(discord.app_commands.Choice(name=name, value=s))
|
| 462 |
+
if not choices:
|
| 463 |
+
choices.append(discord.app_commands.Choice(name=f"🔎 Search: {current}", value=current))
|
| 464 |
+
return choices
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
async def _yt_autocomplete_fetch(query: str) -> list[str]:
|
| 468 |
+
"""Fetch YouTube search suggestions using the suggest endpoint."""
|
| 469 |
+
import aiohttp
|
| 470 |
+
url = f"http://suggestqueries.google.com/complete/search?client=firefox&ds=yt&q={query}"
|
| 471 |
+
try:
|
| 472 |
+
async with aiohttp.ClientSession() as session:
|
| 473 |
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
| 474 |
+
if resp.status == 200:
|
| 475 |
+
data = await resp.json()
|
| 476 |
+
if len(data) >= 2:
|
| 477 |
+
return [str(s) for s in data[1] if isinstance(s, str)]
|
| 478 |
+
except Exception:
|
| 479 |
+
pass
|
| 480 |
+
return []
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
async def _yt_search(query: str, max_results: int = 25) -> list[dict]:
|
| 484 |
+
"""Search YouTube and return video results."""
|
| 485 |
+
import aiohttp
|
| 486 |
+
import json
|
| 487 |
+
import re
|
| 488 |
+
from urllib.parse import quote
|
| 489 |
+
|
| 490 |
+
results = []
|
| 491 |
+
search_url = f"https://www.youtube.com/results?search_query={quote(query)}&hl=en"
|
| 492 |
+
try:
|
| 493 |
+
async with aiohttp.ClientSession() as session:
|
| 494 |
+
async with session.get(search_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
| 495 |
+
if resp.status == 200:
|
| 496 |
+
html = await resp.text()
|
| 497 |
+
match = re.search(r'var ytInitialData\s*=\s*(\{.*?\});', html, re.DOTALL)
|
| 498 |
+
if match:
|
| 499 |
+
data = json.loads(match.group(1))
|
| 500 |
+
contents = data.get("contents", {}).get("twoColumnSearchResultsRenderer", {}).get(
|
| 501 |
+
"primaryContents", {}).get("sectionListRenderer", {}).get("contents", [{}])[0].get(
|
| 502 |
+
"itemSectionRenderer", {}).get("contents", [])
|
| 503 |
+
for item in contents:
|
| 504 |
+
vr = item.get("videoRenderer", {})
|
| 505 |
+
if not vr:
|
| 506 |
+
continue
|
| 507 |
+
video_id = vr.get("videoId", "")
|
| 508 |
+
title_runs = vr.get("title", {}).get("runs", [])
|
| 509 |
+
title = title_runs[0].get("text", "Untitled") if title_runs else "Untitled"
|
| 510 |
+
channel_runs = vr.get("ownerText", {}).get("runs", [])
|
| 511 |
+
channel = channel_runs[0].get("text", "Unknown") if channel_runs else "Unknown"
|
| 512 |
+
view_text = vr.get("viewCountText", {}).get("simpleText", "")
|
| 513 |
+
length_text = vr.get("lengthText", {}).get("simpleText", "")
|
| 514 |
+
thumbnails = vr.get("thumbnail", {}).get("thumbnails", [])
|
| 515 |
+
thumbnail = thumbnails[-1].get("url", "") if thumbnails else ""
|
| 516 |
+
results.append({
|
| 517 |
+
"title": title,
|
| 518 |
+
"channel": channel,
|
| 519 |
+
"views": view_text,
|
| 520 |
+
"duration": length_text,
|
| 521 |
+
"url": f"https://www.youtube.com/watch?v={video_id}",
|
| 522 |
+
"thumbnail": thumbnail,
|
| 523 |
+
"video_id": video_id,
|
| 524 |
+
})
|
| 525 |
+
if len(results) >= max_results:
|
| 526 |
+
break
|
| 527 |
+
except Exception:
|
| 528 |
+
pass
|
| 529 |
+
return results
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
class Utility(commands.Cog):
|
| 533 |
+
"""Utility commands including YouTube search."""
|
| 534 |
+
|
| 535 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 536 |
+
self.bot = bot
|
| 537 |
+
|
| 538 |
+
@commands.hybrid_command(name="botstats", description=get_cmd_desc("commands.tools.botstats_desc"))
|
| 539 |
+
async def botstats(self, ctx: commands.Context) -> None:
|
| 540 |
+
guild_id = ctx.guild.id if ctx.guild else None
|
| 541 |
+
embed = await self.build_botstats_embed(guild_id)
|
| 542 |
+
view = BotStatsView(self, guild_id)
|
| 543 |
+
view.refresh.label = await self.bot.tr(guild_id, "utility.refresh")
|
| 544 |
+
msg = await ctx.reply(embed=embed, view=view)
|
| 545 |
+
if msg is None and ctx.interaction:
|
| 546 |
+
try:
|
| 547 |
+
msg = await ctx.interaction.original_response()
|
| 548 |
+
except Exception:
|
| 549 |
+
msg = None
|
| 550 |
+
view._message = msg
|
| 551 |
+
if msg:
|
| 552 |
+
await view.start_auto_refresh(msg)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
async def setup(bot: commands.Bot) -> None:
|
| 556 |
+
await bot.add_cog(Utility(bot))
|
bot/cogs/verification.py
CHANGED
|
@@ -1,177 +1,178 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import discord
|
| 4 |
-
from discord.ext import commands
|
| 5 |
-
from bot.
|
| 6 |
-
from bot.emojis import ui
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
if
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
await
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
await self.bot.get_text(guild_id, "welcome.
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
item.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
@
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
await channel.set_permissions(
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
await channel.set_permissions(
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
f"- قناة الت
|
| 127 |
-
f"-
|
| 128 |
-
"
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
panel.
|
| 139 |
-
|
| 140 |
-
await
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
@commands.
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import discord
|
| 4 |
+
from discord.ext import commands
|
| 5 |
+
from bot.i18n import get_cmd_desc
|
| 6 |
+
from bot.emojis import ui
|
| 7 |
+
from bot.emojis import ui
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class VerifyView(discord.ui.View):
|
| 11 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 12 |
+
super().__init__(timeout=None)
|
| 13 |
+
self.bot = bot
|
| 14 |
+
|
| 15 |
+
@discord.ui.button(label="Verify", emoji=ui("lock"), style=discord.ButtonStyle.success, custom_id="verify:member")
|
| 16 |
+
async def verify_member(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
|
| 17 |
+
guild_id = interaction.guild.id if interaction.guild else None
|
| 18 |
+
if not interaction.response.is_done():
|
| 19 |
+
try:
|
| 20 |
+
await interaction.response.defer(ephemeral=True, thinking=False)
|
| 21 |
+
except (discord.NotFound, discord.HTTPException, discord.InteractionResponded):
|
| 22 |
+
return
|
| 23 |
+
if not interaction.guild:
|
| 24 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "common.server_only"), ephemeral=True)
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
row = await self.bot.db.fetchone(
|
| 28 |
+
"SELECT verify_role_id, welcome_channel_id FROM guild_config WHERE guild_id = ?",
|
| 29 |
+
interaction.guild.id,
|
| 30 |
+
)
|
| 31 |
+
if not row or not row[0]:
|
| 32 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_role_missing"), ephemeral=True)
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
role = interaction.guild.get_role(row[0])
|
| 36 |
+
if not role:
|
| 37 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_role_deleted"), ephemeral=True)
|
| 38 |
+
return
|
| 39 |
+
|
| 40 |
+
member = interaction.user if isinstance(interaction.user, discord.Member) else interaction.guild.get_member(interaction.user.id)
|
| 41 |
+
if not member:
|
| 42 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_member_missing"), ephemeral=True)
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
if role in member.roles:
|
| 46 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_already_done"), ephemeral=True)
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
await member.add_roles(role, reason="Member completed verification")
|
| 50 |
+
await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_success"), ephemeral=True)
|
| 51 |
+
|
| 52 |
+
await self.bot.log_to_guild(
|
| 53 |
+
interaction.guild,
|
| 54 |
+
await self.bot.get_text(guild_id, "welcome.verify_log_title"),
|
| 55 |
+
await self.bot.get_text(guild_id, "welcome.verify_log_desc", member=member.mention, role=role.mention),
|
| 56 |
+
color=discord.Color.green(),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
async def localize(self, guild_id: int | None) -> "VerifyView":
|
| 60 |
+
for item in self.children:
|
| 61 |
+
if isinstance(item, discord.ui.Button) and item.custom_id == "verify:member":
|
| 62 |
+
item.label = await self.bot.get_text(guild_id, "welcome.verify_button")
|
| 63 |
+
item.emoji = self.bot.get_custom_emoji("admin", fallback="🔐")
|
| 64 |
+
return self
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class Verification(commands.Cog):
|
| 68 |
+
def __init__(self, bot: commands.Bot) -> None:
|
| 69 |
+
self.bot = bot
|
| 70 |
+
|
| 71 |
+
async def cog_load(self) -> None:
|
| 72 |
+
self.bot.add_view(VerifyView(self))
|
| 73 |
+
|
| 74 |
+
@commands.hybrid_command(name="verify", description=get_cmd_desc("commands.tools.verify_desc"))
|
| 75 |
+
async def verify(self, ctx: commands.Context) -> None:
|
| 76 |
+
await self.verify_config(ctx)
|
| 77 |
+
|
| 78 |
+
@commands.hybrid_group(name="verify_config", fallback="panel", description="Verification system controls")
|
| 79 |
+
async def verify_config(self, ctx: commands.Context) -> None:
|
| 80 |
+
"""Open the verification panel."""
|
| 81 |
+
if not ctx.guild:
|
| 82 |
+
await ctx.reply(await self.bot.get_text(None, "common.server_only"))
|
| 83 |
+
return
|
| 84 |
+
embed = await self._build_verify_panel(ctx.guild.id)
|
| 85 |
+
await ctx.reply(embed=embed, view=await VerifyView(self.bot).localize(ctx.guild.id))
|
| 86 |
+
|
| 87 |
+
@verify_config.command(name="setup", description="Configure verification system and lock channels")
|
| 88 |
+
@commands.has_permissions(administrator=True)
|
| 89 |
+
async def verify_setup(
|
| 90 |
+
self,
|
| 91 |
+
ctx: commands.Context,
|
| 92 |
+
role: discord.Role,
|
| 93 |
+
verify_channel: discord.TextChannel,
|
| 94 |
+
welcome_channel: discord.TextChannel,
|
| 95 |
+
) -> None:
|
| 96 |
+
guild = ctx.guild
|
| 97 |
+
|
| 98 |
+
await self.bot.db.execute(
|
| 99 |
+
"""
|
| 100 |
+
INSERT INTO guild_config(guild_id, verify_role_id, verify_channel_id, welcome_channel_id)
|
| 101 |
+
VALUES (?, ?, ?, ?)
|
| 102 |
+
ON CONFLICT(guild_id) DO UPDATE SET
|
| 103 |
+
verify_role_id = excluded.verify_role_id,
|
| 104 |
+
verify_channel_id = excluded.verify_channel_id,
|
| 105 |
+
welcome_channel_id = excluded.welcome_channel_id
|
| 106 |
+
""",
|
| 107 |
+
guild.id,
|
| 108 |
+
role.id,
|
| 109 |
+
verify_channel.id,
|
| 110 |
+
welcome_channel.id,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
everyone = guild.default_role
|
| 114 |
+
for channel in guild.channels:
|
| 115 |
+
if channel.id == verify_channel.id:
|
| 116 |
+
await channel.set_permissions(everyone, view_channel=True, send_messages=False)
|
| 117 |
+
await channel.set_permissions(role, view_channel=True, send_messages=False)
|
| 118 |
+
else:
|
| 119 |
+
await channel.set_permissions(everyone, view_channel=False)
|
| 120 |
+
await channel.set_permissions(role, view_channel=True)
|
| 121 |
+
|
| 122 |
+
embed = discord.Embed(
|
| 123 |
+
title="🔐 Verification Enabled",
|
| 124 |
+
description=(
|
| 125 |
+
"تم تفعيل نظام التحقق الاحترافي.\n"
|
| 126 |
+
f"- قناة التحقق: {verify_channel.mention}\n"
|
| 127 |
+
f"- قناة الترحيب: {welcome_channel.mention}\n"
|
| 128 |
+
f"- رتبة التحقق: {role.mention}\n"
|
| 129 |
+
"كل القنوات أصبحت مخفية حتى يقوم العضو بالتحقق."
|
| 130 |
+
),
|
| 131 |
+
color=discord.Color.blurple(),
|
| 132 |
+
)
|
| 133 |
+
panel = discord.Embed(
|
| 134 |
+
title=await self.bot.get_text(guild.id, "welcome.verify_title"),
|
| 135 |
+
description=await self.bot.get_text(guild.id, "welcome.verify_body"),
|
| 136 |
+
color=discord.Color.green(),
|
| 137 |
+
)
|
| 138 |
+
panel.add_field(name=await self.bot.get_text(guild.id, "welcome.verify_benefits_title"), value=await self.bot.get_text(guild.id, "welcome.verify_benefits_body"), inline=False)
|
| 139 |
+
panel.set_footer(text=await self.bot.get_text(guild.id, "welcome.verify_footer"))
|
| 140 |
+
await verify_channel.send(embed=panel, view=await VerifyView(self.bot).localize(guild.id))
|
| 141 |
+
await ctx.reply(embed=embed)
|
| 142 |
+
|
| 143 |
+
@commands.hybrid_command(name="verifysetup", description=get_cmd_desc("commands.tools.verifysetup_desc"), with_app_command=False)
|
| 144 |
+
@commands.has_permissions(administrator=True)
|
| 145 |
+
async def verify_setup_legacy(
|
| 146 |
+
self,
|
| 147 |
+
ctx: commands.Context,
|
| 148 |
+
role: discord.Role,
|
| 149 |
+
verify_channel: discord.TextChannel,
|
| 150 |
+
welcome_channel: discord.TextChannel,
|
| 151 |
+
) -> None:
|
| 152 |
+
await self.verify_setup(
|
| 153 |
+
ctx,
|
| 154 |
+
role=role,
|
| 155 |
+
verify_channel=verify_channel,
|
| 156 |
+
welcome_channel=welcome_channel,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
async def _build_verify_panel(self, guild_id: int) -> discord.Embed:
|
| 160 |
+
lock = self.bot.get_custom_emoji("admin", fallback="🔐")
|
| 161 |
+
unlock = self.bot.get_custom_emoji("admin", fallback="🔓")
|
| 162 |
+
divider = "⛩️ ━━━ 🏮 ━━━ ⛩️"
|
| 163 |
+
embed = discord.Embed(
|
| 164 |
+
title=f"{lock} {await self.bot.get_text(guild_id, 'welcome.verify_title')}",
|
| 165 |
+
description=f"{divider}\n{await self.bot.get_text(guild_id, 'welcome.verify_body')}\n{divider}",
|
| 166 |
+
color=discord.Color.green(),
|
| 167 |
+
)
|
| 168 |
+
embed.add_field(
|
| 169 |
+
name=f"{unlock} {await self.bot.get_text(guild_id, 'welcome.verify_benefits_title')}",
|
| 170 |
+
value=await self.bot.get_text(guild_id, "welcome.verify_benefits_body"),
|
| 171 |
+
inline=False,
|
| 172 |
+
)
|
| 173 |
+
embed.set_footer(text=await self.bot.get_text(guild_id, "welcome.verify_footer"))
|
| 174 |
+
return embed
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
async def setup(bot: commands.Bot) -> None:
|
| 178 |
+
await bot.add_cog(Verification(bot))
|
bot/config.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import os
|
| 4 |
from dataclasses import dataclass
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
@dataclass(slots=True)
|
| 9 |
class Settings:
|
| 10 |
token: str
|
|
@@ -16,16 +20,44 @@ class Settings:
|
|
| 16 |
gemini_api_key: str = ""
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
def load_settings() -> Settings:
|
| 20 |
load_dotenv()
|
| 21 |
token = os.getenv("DISCORD_TOKEN", "").strip()
|
| 22 |
prefix = os.getenv("PREFIX", "!").strip() or "!"
|
| 23 |
raw_owners = os.getenv("OWNER_IDS", "")
|
| 24 |
-
owner_ids
|
| 25 |
-
int(owner.strip())
|
| 26 |
-
for owner in raw_owners.split(",")
|
| 27 |
-
if owner.strip().isdigit()
|
| 28 |
-
}
|
| 29 |
raw_db_path = os.getenv("DB_PATH", "").strip()
|
| 30 |
if raw_db_path:
|
| 31 |
db_path = raw_db_path
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import logging
|
| 4 |
import os
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
|
| 8 |
|
| 9 |
+
logger = logging.getLogger("mega-bot.config")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
@dataclass(slots=True)
|
| 13 |
class Settings:
|
| 14 |
token: str
|
|
|
|
| 20 |
gemini_api_key: str = ""
|
| 21 |
|
| 22 |
|
| 23 |
+
def _parse_owner_ids(raw: str) -> set[int]:
|
| 24 |
+
"""Parse and validate OWNER_IDS from environment variable."""
|
| 25 |
+
if not raw or not raw.strip():
|
| 26 |
+
logger.warning("OWNER_IDS is empty. No owners configured.")
|
| 27 |
+
return set()
|
| 28 |
+
|
| 29 |
+
owner_ids: set[int] = set()
|
| 30 |
+
for part in raw.split(","):
|
| 31 |
+
cleaned = part.strip()
|
| 32 |
+
if not cleaned:
|
| 33 |
+
continue
|
| 34 |
+
if not cleaned.isdigit():
|
| 35 |
+
logger.warning("Invalid OWNER_IDS value skipped: %r", cleaned)
|
| 36 |
+
continue
|
| 37 |
+
value = int(cleaned)
|
| 38 |
+
# Discord snowflake IDs are positive integers, typically 17-19 digits
|
| 39 |
+
if value <= 0:
|
| 40 |
+
logger.warning("Invalid OWNER_IDS value (non-positive): %d", value)
|
| 41 |
+
continue
|
| 42 |
+
if value > 9_999_999_999_999_999_999:
|
| 43 |
+
logger.warning("Suspiciously large OWNER_IDS value: %d", value)
|
| 44 |
+
continue
|
| 45 |
+
owner_ids.add(value)
|
| 46 |
+
|
| 47 |
+
if not owner_ids:
|
| 48 |
+
logger.warning("No valid OWNER_IDS found after validation.")
|
| 49 |
+
else:
|
| 50 |
+
logger.info("Loaded %d owner ID(s).", len(owner_ids))
|
| 51 |
+
|
| 52 |
+
return owner_ids
|
| 53 |
+
|
| 54 |
+
|
| 55 |
def load_settings() -> Settings:
|
| 56 |
load_dotenv()
|
| 57 |
token = os.getenv("DISCORD_TOKEN", "").strip()
|
| 58 |
prefix = os.getenv("PREFIX", "!").strip() or "!"
|
| 59 |
raw_owners = os.getenv("OWNER_IDS", "")
|
| 60 |
+
owner_ids = _parse_owner_ids(raw_owners)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
raw_db_path = os.getenv("DB_PATH", "").strip()
|
| 62 |
if raw_db_path:
|
| 63 |
db_path = raw_db_path
|
bot/database.py
CHANGED
|
@@ -6,6 +6,7 @@ Provides async SQLite operations with schema migrations and persistent connectio
|
|
| 6 |
import asyncio
|
| 7 |
import os
|
| 8 |
import shutil
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
import aiosqlite
|
|
@@ -17,6 +18,10 @@ except Exception: # pragma: no cover
|
|
| 17 |
hf_hub_download = None
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
class Database:
|
| 21 |
def __init__(self, path: str = "database.db") -> None:
|
| 22 |
self.path = path
|
|
@@ -37,6 +42,7 @@ class Database:
|
|
| 37 |
and HfApi
|
| 38 |
and hf_hub_download
|
| 39 |
)
|
|
|
|
| 40 |
|
| 41 |
def _prepare_path(self) -> None:
|
| 42 |
"""Ensure the sqlite parent directory exists when a directory is provided."""
|
|
@@ -66,6 +72,11 @@ class Database:
|
|
| 66 |
async def _push_remote_db(self) -> None:
|
| 67 |
if not self._hf_sync_enabled:
|
| 68 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
try:
|
| 70 |
api = HfApi(token=self._hf_token)
|
| 71 |
await asyncio.to_thread(
|
|
@@ -76,6 +87,7 @@ class Database:
|
|
| 76 |
repo_type=self._hf_repo_type,
|
| 77 |
commit_message="Bot DB sync",
|
| 78 |
)
|
|
|
|
| 79 |
except Exception:
|
| 80 |
# Disable further sync attempts to avoid repeated noisy auth/network failures.
|
| 81 |
self._hf_sync_enabled = False
|
|
@@ -350,6 +362,17 @@ class Database:
|
|
| 350 |
updated_by INTEGER,
|
| 351 |
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 352 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
"""
|
| 354 |
)
|
| 355 |
|
|
|
|
| 6 |
import asyncio
|
| 7 |
import os
|
| 8 |
import shutil
|
| 9 |
+
import time
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
import aiosqlite
|
|
|
|
| 18 |
hf_hub_download = None
|
| 19 |
|
| 20 |
|
| 21 |
+
# Minimum seconds between HuggingFace DB sync pushes to avoid rate limits
|
| 22 |
+
_HF_SYNC_INTERVAL_SECONDS = 60
|
| 23 |
+
|
| 24 |
+
|
| 25 |
class Database:
|
| 26 |
def __init__(self, path: str = "database.db") -> None:
|
| 27 |
self.path = path
|
|
|
|
| 42 |
and HfApi
|
| 43 |
and hf_hub_download
|
| 44 |
)
|
| 45 |
+
self._last_hf_push: float = 0.0
|
| 46 |
|
| 47 |
def _prepare_path(self) -> None:
|
| 48 |
"""Ensure the sqlite parent directory exists when a directory is provided."""
|
|
|
|
| 72 |
async def _push_remote_db(self) -> None:
|
| 73 |
if not self._hf_sync_enabled:
|
| 74 |
return
|
| 75 |
+
# Rate limit: avoid pushing too frequently
|
| 76 |
+
now = time.monotonic()
|
| 77 |
+
elapsed = now - self._last_hf_push
|
| 78 |
+
if elapsed < _HF_SYNC_INTERVAL_SECONDS:
|
| 79 |
+
return
|
| 80 |
try:
|
| 81 |
api = HfApi(token=self._hf_token)
|
| 82 |
await asyncio.to_thread(
|
|
|
|
| 87 |
repo_type=self._hf_repo_type,
|
| 88 |
commit_message="Bot DB sync",
|
| 89 |
)
|
| 90 |
+
self._last_hf_push = time.monotonic()
|
| 91 |
except Exception:
|
| 92 |
# Disable further sync attempts to avoid repeated noisy auth/network failures.
|
| 93 |
self._hf_sync_enabled = False
|
|
|
|
| 362 |
updated_by INTEGER,
|
| 363 |
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 364 |
);
|
| 365 |
+
|
| 366 |
+
CREATE TABLE IF NOT EXISTS ai_scheduled_tasks (
|
| 367 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 368 |
+
guild_id INTEGER,
|
| 369 |
+
run_at TEXT,
|
| 370 |
+
action_json TEXT,
|
| 371 |
+
reason TEXT,
|
| 372 |
+
created_by INTEGER,
|
| 373 |
+
executed INTEGER DEFAULT 0,
|
| 374 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 375 |
+
);
|
| 376 |
"""
|
| 377 |
)
|
| 378 |
|
bot/emojis.py
CHANGED
|
@@ -3,6 +3,7 @@ Enhanced Emoji system for the bot.
|
|
| 3 |
Loads custom emojis from emojies.txt and provides UI emoji aliases with rich decorations.
|
| 4 |
"""
|
| 5 |
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
import re
|
| 8 |
|
|
@@ -79,8 +80,9 @@ FALLBACK_EMOJIS: dict[str, str] = {
|
|
| 79 |
_EMOJI_FILES = (
|
| 80 |
Path("emojies.txt"),
|
| 81 |
Path("bot/emojies.txt"),
|
| 82 |
-
Path("/
|
| 83 |
)
|
|
|
|
| 84 |
_EMOJI_ID_RE = re.compile(r"\d{6,}")
|
| 85 |
_EMOJI_TAG_RE = re.compile(r"^<(a?):([\w~]+):(\d{6,})>$")
|
| 86 |
_EMOJI_BOT: discord.Client | None = None
|
|
@@ -139,7 +141,11 @@ def _ensure_unescaped_emoji(value: str) -> str:
|
|
| 139 |
|
| 140 |
|
| 141 |
def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Client | None = None) -> str:
|
| 142 |
-
"""Resolve emoji config with a hybrid resolver (cache first, then
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
parsed = _parse_custom_emoji_config(value)
|
| 144 |
if parsed is not None:
|
| 145 |
config_name, emoji_id, animated = parsed
|
|
@@ -148,7 +154,8 @@ def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Clien
|
|
| 148 |
resolved = active_bot.get_emoji(emoji_id)
|
| 149 |
if resolved is not None:
|
| 150 |
return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
|
| 151 |
-
return
|
|
|
|
| 152 |
|
| 153 |
emoji_id = _extract_emoji_id(value)
|
| 154 |
active_bot = bot or _EMOJI_BOT
|
|
@@ -156,6 +163,8 @@ def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Clien
|
|
| 156 |
resolved = active_bot.get_emoji(emoji_id)
|
| 157 |
if resolved is not None:
|
| 158 |
return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
|
|
|
|
|
|
|
| 159 |
return _ensure_unescaped_emoji(value or fallback)
|
| 160 |
|
| 161 |
|
|
@@ -488,7 +497,6 @@ COMMAND_EMOJIS: dict[str, str] = {
|
|
| 488 |
"seek": "⏩",
|
| 489 |
"rewind": "⏪",
|
| 490 |
"forward": "⏩",
|
| 491 |
-
"rewind": "⏪",
|
| 492 |
"filter": "🎛️",
|
| 493 |
"clear": ui("trash"),
|
| 494 |
"remove": ui("spotify_remove"),
|
|
|
|
| 3 |
Loads custom emojis from emojies.txt and provides UI emoji aliases with rich decorations.
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
import os
|
| 7 |
from pathlib import Path
|
| 8 |
import re
|
| 9 |
|
|
|
|
| 80 |
_EMOJI_FILES = (
|
| 81 |
Path("emojies.txt"),
|
| 82 |
Path("bot/emojies.txt"),
|
| 83 |
+
Path(os.getenv("EMOJI_FILE_PATH", "/opt/bot/emojies.txt")) if os.getenv("EMOJI_FILE_PATH") else None,
|
| 84 |
)
|
| 85 |
+
_EMOJI_FILES = tuple(p for p in _EMOJI_FILES if p is not None)
|
| 86 |
_EMOJI_ID_RE = re.compile(r"\d{6,}")
|
| 87 |
_EMOJI_TAG_RE = re.compile(r"^<(a?):([\w~]+):(\d{6,})>$")
|
| 88 |
_EMOJI_BOT: discord.Client | None = None
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Client | None = None) -> str:
|
| 144 |
+
"""Resolve emoji config with a hybrid resolver (cache first, then fallback to unicode).
|
| 145 |
+
|
| 146 |
+
If the custom emoji is available in the bot's cache, it returns the full tag.
|
| 147 |
+
Otherwise it falls back to the unicode fallback to avoid showing raw :name: text.
|
| 148 |
+
"""
|
| 149 |
parsed = _parse_custom_emoji_config(value)
|
| 150 |
if parsed is not None:
|
| 151 |
config_name, emoji_id, animated = parsed
|
|
|
|
| 154 |
resolved = active_bot.get_emoji(emoji_id)
|
| 155 |
if resolved is not None:
|
| 156 |
return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
|
| 157 |
+
# Emoji not in cache — return fallback unicode to avoid raw :name: display
|
| 158 |
+
return fallback
|
| 159 |
|
| 160 |
emoji_id = _extract_emoji_id(value)
|
| 161 |
active_bot = bot or _EMOJI_BOT
|
|
|
|
| 163 |
resolved = active_bot.get_emoji(emoji_id)
|
| 164 |
if resolved is not None:
|
| 165 |
return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
|
| 166 |
+
return fallback
|
| 167 |
+
# Value might be a plain unicode emoji or already-resolved string
|
| 168 |
return _ensure_unescaped_emoji(value or fallback)
|
| 169 |
|
| 170 |
|
|
|
|
| 497 |
"seek": "⏩",
|
| 498 |
"rewind": "⏪",
|
| 499 |
"forward": "⏩",
|
|
|
|
| 500 |
"filter": "🎛️",
|
| 501 |
"clear": ui("trash"),
|
| 502 |
"remove": ui("spotify_remove"),
|