feat: Add Neru Bot with global command syncing and environment variable support
This commit is contained in:
parent
aa6c4511d8
commit
121fac424a
27
README.md
27
README.md
@ -46,33 +46,34 @@ A versatile, modular Discord bot framework with multiple bot personalities and e
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up environment variables**
|
3. **Set up environment variables**
|
||||||
|
|
||||||
Create a `.env` file in the root directory with the following variables:
|
Create a `.env` file in the root directory with the following variables:
|
||||||
```
|
```
|
||||||
# Required
|
# Required
|
||||||
DISCORD_TOKEN=your_main_discord_bot_token
|
DISCORD_TOKEN=your_main_discord_bot_token
|
||||||
OWNER_USER_ID=your_discord_user_id
|
OWNER_USER_ID=your_discord_user_id
|
||||||
|
|
||||||
# For specific bots (optional)
|
# For specific bots (optional)
|
||||||
DISCORD_TOKEN_GURT=your_gurt_bot_token
|
DISCORD_TOKEN_GURT=your_gurt_bot_token
|
||||||
DISCORD_TOKEN_WHEATLEY=your_wheatley_bot_token
|
DISCORD_TOKEN_WHEATLEY=your_wheatley_bot_token
|
||||||
DISCORD_TOKEN_NERU=your_neru_bot_token
|
NERU_BOT_TOKEN=your_neru_bot_token
|
||||||
|
DISCORD_TOKEN_NERU=your_neru_bot_token # Alternative to NERU_BOT_TOKEN
|
||||||
DISCORD_TOKEN_MIKU=your_miku_bot_token
|
DISCORD_TOKEN_MIKU=your_miku_bot_token
|
||||||
|
|
||||||
# For AI features (if using)
|
# For AI features (if using)
|
||||||
AI_API_KEY=your_openrouter_api_key
|
AI_API_KEY=your_openrouter_api_key
|
||||||
GCP_PROJECT_ID=your_gcp_project_id
|
GCP_PROJECT_ID=your_gcp_project_id
|
||||||
GCP_LOCATION=us-central1
|
GCP_LOCATION=us-central1
|
||||||
|
|
||||||
# For web search (optional)
|
# For web search (optional)
|
||||||
TAVILY_API_KEY=your_tavily_api_key
|
TAVILY_API_KEY=your_tavily_api_key
|
||||||
|
|
||||||
# For database (if using)
|
# For database (if using)
|
||||||
POSTGRES_USER=your_postgres_user
|
POSTGRES_USER=your_postgres_user
|
||||||
POSTGRES_PASSWORD=your_postgres_password
|
POSTGRES_PASSWORD=your_postgres_password
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
POSTGRES_SETTINGS_DB=discord_bot_settings
|
POSTGRES_SETTINGS_DB=discord_bot_settings
|
||||||
|
|
||||||
# For Redis (if using)
|
# For Redis (if using)
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
@ -109,6 +110,11 @@ python run_wheatley_bot.py
|
|||||||
python run_additional_bots.py
|
python run_additional_bots.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Neru Bot**
|
||||||
|
```bash
|
||||||
|
python run_neru_bot.py
|
||||||
|
```
|
||||||
|
|
||||||
### API Service
|
### API Service
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -137,9 +143,16 @@ python api_service/api_server.py
|
|||||||
- Use `/multibot stop <bot_id>` to stop a specific bot
|
- Use `/multibot stop <bot_id>` to stop a specific bot
|
||||||
- Use `/multibot startall` to start all configured bots
|
- Use `/multibot startall` to start all configured bots
|
||||||
|
|
||||||
|
### Neru Bot
|
||||||
|
- Default prefix: `!` (same as main bot)
|
||||||
|
- Uses global command syncing instead of per-guild
|
||||||
|
- All commands work in DMs and private channels
|
||||||
|
- Identical functionality to main bot but with different command registration
|
||||||
|
|
||||||
## 🧩 Project Structure
|
## 🧩 Project Structure
|
||||||
|
|
||||||
- **`main.py`**: Main bot entry point
|
- **`main.py`**: Main bot entry point
|
||||||
|
- **`neru_bot.py`**: Global command syncing bot entry point
|
||||||
- **`gurt_bot.py`/`wheatley_bot.py`**: Specialized bot entry points
|
- **`gurt_bot.py`/`wheatley_bot.py`**: Specialized bot entry points
|
||||||
- **`multi_bot.py`**: Multi-bot system for running multiple AI personalities
|
- **`multi_bot.py`**: Multi-bot system for running multiple AI personalities
|
||||||
- **`cogs/`**: Directory containing different modules (cogs)
|
- **`cogs/`**: Directory containing different modules (cogs)
|
||||||
|
193
neru_bot.py
Normal file
193
neru_bot.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import asyncpg
|
||||||
|
import redis.asyncio as aioredis
|
||||||
|
from commands import load_all_cogs
|
||||||
|
from error_handler import handle_error
|
||||||
|
import settings_manager
|
||||||
|
from global_bot_accessor import set_bot_instance
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# --- Constants ---
|
||||||
|
DEFAULT_PREFIX = "!"
|
||||||
|
CORE_COGS = {'SettingsCog', 'HelpCog'} # Cogs that cannot be disabled
|
||||||
|
|
||||||
|
# --- Dynamic Prefix Function ---
|
||||||
|
async def get_prefix(bot_instance, message):
|
||||||
|
"""Determines the command prefix based on guild settings or default, but disables mention as prefix."""
|
||||||
|
if not message.guild:
|
||||||
|
# Use default prefix in DMs
|
||||||
|
return DEFAULT_PREFIX
|
||||||
|
|
||||||
|
# Fetch prefix from settings manager (cache first, then DB)
|
||||||
|
prefix = await settings_manager.get_guild_prefix(message.guild.id, DEFAULT_PREFIX)
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
# --- Bot Setup ---
|
||||||
|
# Set up intents (permissions)
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
# --- Custom Bot Class with setup_hook for async initialization ---
|
||||||
|
class NeruBot(commands.Bot):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
owner_id = os.getenv('OWNER_USER_ID')
|
||||||
|
if owner_id:
|
||||||
|
self.owner_id = int(owner_id)
|
||||||
|
self.core_cogs = CORE_COGS # Attach core cogs list to bot instance
|
||||||
|
self.settings_manager = settings_manager # Attach settings manager instance
|
||||||
|
self.pg_pool = None # Will be initialized in setup_hook
|
||||||
|
self.redis = None # Will be initialized in setup_hook
|
||||||
|
self.ai_cogs_to_skip = [] # For --disable-ai flag
|
||||||
|
|
||||||
|
async def setup_hook(self):
|
||||||
|
"""Async initialization that runs after login but before on_ready."""
|
||||||
|
log.info("NeruBot setup_hook called")
|
||||||
|
|
||||||
|
# Initialize database connections
|
||||||
|
try:
|
||||||
|
# PostgreSQL connection pool
|
||||||
|
self.pg_pool = await asyncpg.create_pool(
|
||||||
|
user=os.getenv('POSTGRES_USER'),
|
||||||
|
password=os.getenv('POSTGRES_PASSWORD'),
|
||||||
|
host=os.getenv('POSTGRES_HOST'),
|
||||||
|
database=os.getenv('POSTGRES_SETTINGS_DB')
|
||||||
|
)
|
||||||
|
log.info("PostgreSQL connection pool initialized")
|
||||||
|
|
||||||
|
# Redis connection
|
||||||
|
self.redis = await aioredis.from_url(
|
||||||
|
f"redis://{os.getenv('REDIS_HOST')}:{os.getenv('REDIS_PORT', '6379')}",
|
||||||
|
password=os.getenv('REDIS_PASSWORD'),
|
||||||
|
decode_responses=True
|
||||||
|
)
|
||||||
|
log.info("Redis connection initialized")
|
||||||
|
|
||||||
|
# Initialize database schema and run migrations using settings_manager
|
||||||
|
if self.pg_pool and self.redis:
|
||||||
|
try:
|
||||||
|
await settings_manager.initialize_database()
|
||||||
|
log.info("Database schema initialization called via settings_manager.")
|
||||||
|
await settings_manager.run_migrations()
|
||||||
|
log.info("Database migrations called via settings_manager.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("CRITICAL: Failed during settings_manager database setup (init/migrations).")
|
||||||
|
else:
|
||||||
|
log.error("CRITICAL: pg_pool or redis_client not initialized in setup_hook. Cannot proceed with settings_manager setup.")
|
||||||
|
|
||||||
|
# Load all cogs
|
||||||
|
try:
|
||||||
|
await load_all_cogs(self)
|
||||||
|
log.info("All cogs loaded successfully")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error loading cogs: {e}")
|
||||||
|
|
||||||
|
# Apply global allowed_installs and allowed_contexts to all commands
|
||||||
|
try:
|
||||||
|
log.info("Applying global allowed_installs and allowed_contexts to all commands...")
|
||||||
|
for command in self.tree.get_commands():
|
||||||
|
# Apply decorators to each command
|
||||||
|
app_commands.allowed_installs(guilds=True, users=True)(command)
|
||||||
|
app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)(command)
|
||||||
|
|
||||||
|
# Sync commands globally
|
||||||
|
log.info("Starting global command sync process...")
|
||||||
|
synced = await self.tree.sync()
|
||||||
|
log.info(f"Synced {len(synced)} commands globally")
|
||||||
|
|
||||||
|
# List commands after sync
|
||||||
|
commands_after = [cmd.name for cmd in self.tree.get_commands()]
|
||||||
|
log.info(f"Commands registered in command tree: {commands_after}")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to sync commands: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error in setup_hook: {e}")
|
||||||
|
|
||||||
|
# Create bot instance using the custom class
|
||||||
|
bot = NeruBot(command_prefix=get_prefix, intents=intents)
|
||||||
|
|
||||||
|
# --- Logging Setup ---
|
||||||
|
# Configure logging (adjust level and format as needed)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
|
||||||
|
log = logging.getLogger(__name__) # Logger for neru_bot.py
|
||||||
|
|
||||||
|
# --- Events ---
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
if bot.user:
|
||||||
|
log.info(f'{bot.user.name} has connected to Discord!')
|
||||||
|
log.info(f'Bot ID: {bot.user.id}')
|
||||||
|
# Set the bot's status
|
||||||
|
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="!help"))
|
||||||
|
log.info("Bot status set to 'Listening to !help'")
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
@bot.event
|
||||||
|
async def on_command_error(ctx, error):
|
||||||
|
await handle_error(ctx, error)
|
||||||
|
|
||||||
|
@bot.tree.error
|
||||||
|
async def on_app_command_error(interaction, error):
|
||||||
|
await handle_error(interaction, error)
|
||||||
|
|
||||||
|
async def main(args):
|
||||||
|
"""Main async function to load cogs and start the bot."""
|
||||||
|
TOKEN = os.getenv('NERU_BOT_TOKEN')
|
||||||
|
if not TOKEN:
|
||||||
|
raise ValueError("No token found. Make sure to set NERU_BOT_TOKEN in your .env file.")
|
||||||
|
|
||||||
|
# Set the global bot instance for other modules to access
|
||||||
|
set_bot_instance(bot)
|
||||||
|
log.info(f"Global bot instance set in global_bot_accessor. Bot ID: {id(bot)}")
|
||||||
|
|
||||||
|
# Configure AI cogs to skip if needed
|
||||||
|
if args.disable_ai:
|
||||||
|
log.info("AI functionality disabled via command line flag.")
|
||||||
|
ai_cogs_to_skip = [
|
||||||
|
"cogs.ai_cog",
|
||||||
|
"cogs.multi_conversation_ai_cog",
|
||||||
|
# Add any other AI-related cogs from the 'cogs' folder here
|
||||||
|
]
|
||||||
|
# Store the skip list on the bot object for reload commands
|
||||||
|
bot.ai_cogs_to_skip = ai_cogs_to_skip
|
||||||
|
else:
|
||||||
|
bot.ai_cogs_to_skip = [] # Ensure it exists even if empty
|
||||||
|
|
||||||
|
try:
|
||||||
|
# The bot will call setup_hook internally after login but before on_ready.
|
||||||
|
await bot.start(TOKEN)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"An error occurred during bot.start(): {e}")
|
||||||
|
finally:
|
||||||
|
# Close database connections
|
||||||
|
if bot.pg_pool:
|
||||||
|
await bot.pg_pool.close()
|
||||||
|
log.info("PostgreSQL connection pool closed.")
|
||||||
|
if bot.redis:
|
||||||
|
await bot.redis.close()
|
||||||
|
log.info("Redis connection closed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
parser = argparse.ArgumentParser(description="Run the Neru Discord Bot.")
|
||||||
|
parser.add_argument('--disable-ai', action='store_true', help='Disable AI functionality')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(main(args))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Bot stopped by user.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"An error occurred running the bot: {e}")
|
@ -6,6 +6,7 @@ import threading
|
|||||||
import asyncio
|
import asyncio
|
||||||
import multi_bot
|
import multi_bot
|
||||||
import gurt_bot
|
import gurt_bot
|
||||||
|
import neru_bot
|
||||||
|
|
||||||
def run_gurt_bot_in_thread():
|
def run_gurt_bot_in_thread():
|
||||||
"""Run the Gurt Bot in a separate thread"""
|
"""Run the Gurt Bot in a separate thread"""
|
||||||
@ -14,6 +15,15 @@ def run_gurt_bot_in_thread():
|
|||||||
thread.start()
|
thread.start()
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
|
def run_neru_bot_in_thread():
|
||||||
|
"""Run the Neru Bot in a separate thread"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
# Create args object with disable_ai=False
|
||||||
|
args = type('Args', (), {'disable_ai': False})()
|
||||||
|
thread = threading.Thread(target=lambda: loop.run_until_complete(neru_bot.main(args)), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to run all additional bots"""
|
"""Main function to run all additional bots"""
|
||||||
print("Starting additional bots (Neru, Miku, and Gurt)...")
|
print("Starting additional bots (Neru, Miku, and Gurt)...")
|
||||||
@ -25,6 +35,10 @@ def main():
|
|||||||
gurt_thread = run_gurt_bot_in_thread()
|
gurt_thread = run_gurt_bot_in_thread()
|
||||||
bot_threads.append(("gurt", gurt_thread))
|
bot_threads.append(("gurt", gurt_thread))
|
||||||
|
|
||||||
|
# Start Neru Bot
|
||||||
|
neru_thread = run_neru_bot_in_thread()
|
||||||
|
bot_threads.append(("neru", neru_thread))
|
||||||
|
|
||||||
if not bot_threads:
|
if not bot_threads:
|
||||||
print("No bots were started. Check your configuration in data/multi_bot_config.json")
|
print("No bots were started. Check your configuration in data/multi_bot_config.json")
|
||||||
return
|
return
|
||||||
@ -41,6 +55,8 @@ def main():
|
|||||||
print(f"Thread for bot {bot_id} died, restarting...")
|
print(f"Thread for bot {bot_id} died, restarting...")
|
||||||
if bot_id == "gurt":
|
if bot_id == "gurt":
|
||||||
new_thread = run_gurt_bot_in_thread()
|
new_thread = run_gurt_bot_in_thread()
|
||||||
|
elif bot_id == "neru":
|
||||||
|
new_thread = run_neru_bot_in_thread()
|
||||||
else:
|
else:
|
||||||
new_thread = multi_bot.run_bot_in_thread(bot_id)
|
new_thread = multi_bot.run_bot_in_thread(bot_id)
|
||||||
bot_threads.remove((bot_id, thread))
|
bot_threads.remove((bot_id, thread))
|
||||||
|
18
run_neru_bot.py
Normal file
18
run_neru_bot.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import argparse
|
||||||
|
import neru_bot
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Run the Neru Discord Bot.")
|
||||||
|
parser.add_argument('--disable-ai', action='store_true', help='Disable AI functionality')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pass the arguments to the main function
|
||||||
|
asyncio.run(neru_bot.main(args))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Neru Bot stopped by user.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred running Neru Bot: {e}")
|
Loading…
x
Reference in New Issue
Block a user