-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbot.py
More file actions
313 lines (266 loc) · 11.8 KB
/
Copy pathbot.py
File metadata and controls
313 lines (266 loc) · 11.8 KB
1
2
3
4
5
6
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
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
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
267
268
269
270
271
272
273
274
275
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
310
311
312
313
#!/usr/bin/env python3
"""
Full-featured DiscordRPG Discord Bot
Implements all major features from the original DiscordRPG
"""
import asyncio
import logging
import os
from datetime import datetime, timezone, timedelta
from typing import Optional
import discord
from discord.ext import commands
from dotenv import load_dotenv
# EST timezone
EST = timezone(timedelta(hours=-5))
from utils.database import Database
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('DiscordRPG')
class DiscordRPGBot(commands.Bot):
"""Main bot class with all DiscordRPG features"""
def __init__(self):
# Bot configuration - ENABLE PRIVILEGED INTENTS IN DISCORD DEVELOPER PORTAL
# Go to https://discord.com/developers/applications/ -> Your App -> Bot -> Privileged Gateway Intents
# Enable "Message Content Intent", "Server Members Intent", and "Presence Intent" for full functionality
intents = discord.Intents.default()
intents.message_content = True # Required for commands to work
intents.members = True # Needed for auto-registration - re-enabled
intents.dm_messages = True # Allow DM commands
intents.presences = True # Required to see user status (online/offline/idle/dnd)
super().__init__(
command_prefix=self.get_prefix,
intents=intents,
help_command=None,
description="A full-featured DiscordRPG bot for Discord"
)
# Get configuration from environment
self.token = os.getenv('DISCORD_TOKEN')
self.prefix = os.getenv('BOT_PREFIX', '!')
self.db_path = os.getenv('DATABASE_PATH', './discordrpg.db') # Legacy, ignored by MariaDB Database class
if not self.token:
logger.error("DISCORD_TOKEN not found in environment variables!")
raise ValueError("Missing Discord token")
# Database connection
self.db: Optional[Database] = None
# Cache for various data
self.prefixes = {} # Guild-specific prefixes
self.cooldowns = {} # User cooldowns
self.adventures = {} # Active adventures
# Constants
self.primary_color = discord.Color(0xFF6B6B)
self.error_color = discord.Color(0xFF0000)
self.success_color = discord.Color(0x00FF00)
async def get_prefix(self, message: discord.Message) -> str:
"""Get the prefix for a guild or DM"""
if not message.guild:
return self.prefix # Use default prefix in DMs
if message.guild.id not in self.prefixes:
# Load from database
if self.db:
row = self.db.fetchone(
"SELECT prefix FROM server_settings WHERE guild_id = ?",
(message.guild.id,)
)
# Safely access dict values
prefix = self.db.row_to_dict(row)['prefix'] if row else self.prefix
self.prefixes[message.guild.id] = prefix
else:
self.prefixes[message.guild.id] = self.prefix
return self.prefixes[message.guild.id]
async def setup_hook(self):
"""Initialize bot components"""
# Connect to MariaDB database
self.db = Database(self.db_path)
self.db.init_database()
logger.info("Initialized MariaDB database connection")
# Load cogs
await self.load_cogs()
async def load_cogs(self):
"""Load all cog extensions"""
# Load all available cogs
cog_files = [
"cogs.auto_register", # Auto-registration and penalties
"cogs.character",
"cogs.help",
"cogs.inventory",
"cogs.combat",
"cogs.epic_adventures", # Epic and legendary adventures
"cogs.economy",
"cogs.daily",
"cogs.achievements", # Player achievement tracking
"cogs.quests_board", # Daily/weekly objective board
"cogs.gambling",
"cogs.religion", # Gods, prayer, and sacrifice
"cogs.race", # Race selection and bonuses
"cogs.autoplay", # Automatic gameplay system
"cogs.raids", # Automatic raid system
"cogs.oracle", # AI-powered game manual and help system
"cogs.ai_events", # AI-powered dynamic event generation
"cogs.backup", # Database backup system
"cogs.personal_quests", # AI-generated personal quest lines
]
for cog in cog_files:
try:
await self.load_extension(cog)
logger.info(f"Loaded cog: {cog}")
except Exception as e:
logger.error(f"Failed to load cog {cog}: {e}")
async def on_ready(self):
"""Bot is ready"""
logger.info(f"Logged in as {self.user} (ID: {self.user.id})")
logger.info(f"Connected to {len(self.guilds)} guilds")
# Set status
await self.change_presence(
activity=discord.Game(name=f"{self.prefix}help | DiscordRPG"),
status=discord.Status.online
)
async def on_guild_join(self, guild: discord.Guild):
"""Bot joined a new guild"""
logger.info(f"Joined guild: {guild.name} (ID: {guild.id})")
# Create server settings
if self.db:
self.db.execute(
"""INSERT OR IGNORE INTO server_settings (guild_id, prefix)
VALUES (?, ?)""",
(guild.id, self.prefix)
)
self.db.commit()
async def process_commands(self, message: discord.Message):
"""Process commands with channel restrictions"""
# Only process commands in DMs or the designated discordrpg channel
if message.guild is not None: # Not a DM
if message.channel.name.lower() not in ['discordrpg', 'rpg', 'game', 'bot']:
return # Ignore commands in other channels
await super().process_commands(message)
async def on_command_error(self, ctx: commands.Context, error: Exception):
"""Handle command errors"""
if isinstance(error, commands.CommandNotFound):
return
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send(f"❌ Missing required argument: `{error.param.name}`")
elif isinstance(error, commands.BadArgument):
await ctx.send(f"❌ Invalid argument: {error}")
elif isinstance(error, commands.CommandOnCooldown):
await ctx.send(f"⏰ Command on cooldown. Try again in {error.retry_after:.1f}s")
elif isinstance(error, commands.CheckFailure):
await ctx.send("❌ You don't have permission to use this command")
elif isinstance(error, commands.CommandInvokeError):
original = error.original
logger.error(f"Command {ctx.command} failed: {original}", exc_info=original)
await ctx.send("❌ That command hit a runtime error. The ravens have logged the stack trace.")
else:
logger.error(f"Unhandled error in {ctx.command}: {error}", exc_info=error)
await ctx.send("❌ An unexpected error occurred")
async def close(self):
"""Cleanup on bot shutdown"""
if self.db:
self.db.close()
await super().close()
def run(self):
"""Run the bot"""
super().run(self.token)
# Utility functions for cogs
class DiscordRPGCog(commands.Cog):
"""Base class for DiscordRPG cogs"""
def __init__(self, bot: DiscordRPGBot):
self.bot = bot
@property
def db(self) -> Database:
return self.bot.db
def has_character(self, user_id: int) -> bool:
"""Check if user has a character"""
char = self.db.get_character(user_id)
return char is not None
def get_character_field(self, user_id: int, field: str):
"""Get a specific field from character"""
# Validate field name to prevent SQL injection
valid_fields = {
'user_id', 'name', 'level', 'xp', 'money', 'race', 'class', 'luck',
'pvpwins', 'pvplosses', 'deaths', 'kills', 'completed', 'god', 'favor',
'marriage', 'guild', 'alignment', 'streak', 'last_date', 'last_adventure',
'crates_common', 'crates_uncommon', 'crates_rare', 'crates_magic',
'crates_legendary', 'crates_mystery', 'atkmultiply', 'defmultiply',
'raidstats', 'has_character', 'reset_points', 'created_at',
'sell_confirmation', 'adventure_alert',
}
if field not in valid_fields:
return None
row = self.db.fetchone(
f"SELECT {field} FROM profile WHERE user_id = ?",
(user_id,)
)
return self.db.row_to_dict(row)[field] if row else None
def embed(self, title: str, description: str = None,
color: Optional[discord.Color] = None) -> discord.Embed:
"""Create a standard embed"""
return discord.Embed(
title=title,
description=description,
color=color or self.bot.primary_color,
timestamp=datetime.now(EST)
)
def success_embed(self, description: str) -> discord.Embed:
"""Create a success embed"""
return self.embed("✅ Success", description, self.bot.success_color)
def error_embed(self, description: str) -> discord.Embed:
"""Create an error embed"""
return self.embed("❌ Error", description, self.bot.error_color)
# Character check decorator
def has_character():
"""Check if user has a character"""
async def predicate(ctx: commands.Context):
if ctx.bot.db:
char = ctx.bot.db.get_character(ctx.author.id)
if not char:
await ctx.send("❌ You need to create a character first! Use `!create`")
return False
return True
return False
return commands.check(predicate)
# Add confirmation method to Context
async def confirm(ctx: commands.Context, message: str, timeout: float = 30.0) -> bool:
"""Ask user to confirm an action"""
embed = discord.Embed(
title="❓ Confirmation",
description=f"{message}\n\nReact with ✅ to confirm or ❌ to cancel.",
color=0xFFAA00
)
msg = await ctx.send(embed=embed)
await msg.add_reaction("✅")
await msg.add_reaction("❌")
def check(reaction, user):
return (user == ctx.author and
str(reaction.emoji) in ["✅", "❌"] and
reaction.message.id == msg.id)
try:
reaction, user = await ctx.bot.wait_for('reaction_add', timeout=timeout, check=check)
await msg.delete()
return str(reaction.emoji) == "✅"
except asyncio.TimeoutError:
await msg.delete()
return False
# Monkey patch the confirmation method onto Context
commands.Context.confirm = confirm
# Cooldown check
def cooldown_check(cooldown_name: str, seconds: int):
"""Check if user is on cooldown for specific action"""
async def predicate(ctx: commands.Context):
cooldowns = ctx.bot.db.get_cooldowns(ctx.author.id)
last_use = cooldowns.get(cooldown_name)
if last_use:
time_passed = (datetime.now() - last_use).total_seconds()
if time_passed < seconds:
remaining = seconds - time_passed
await ctx.send(f"⏰ You can use this again in {remaining:.0f} seconds")
return False
return True
return commands.check(predicate)
if __name__ == "__main__":
bot = DiscordRPGBot()
bot.run()