Add multi-channel support for stock updates

Bot now supports sending updates to multiple Discord channels simultaneously.

Changes:
- CHANNEL_ID accepts comma-separated list of channel IDs
- Config parses and validates multiple channel IDs
- Hourly and market close updates sent to all configured channels
- Update .env.example to show multi-channel format
- Document multi-channel configuration in README

Example: CHANNEL_ID=1442203998932304035,9876543210123456789

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Simard
2025-12-03 19:01:01 -06:00
parent 584ad2a4f4
commit 327e7e0914
4 changed files with 28 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
# Discord Configuration # Discord Configuration
DISCORD_TOKEN=your_discord_bot_token_here DISCORD_TOKEN=your_discord_bot_token_here
CHANNEL_ID=your_channel_id_here CHANNEL_ID=your_channel_id_here,optional_second_channel_id
# Bot Configuration # Bot Configuration
COMMAND_PREFIX=! COMMAND_PREFIX=!

View File

@@ -159,11 +159,16 @@ Environment variables in `.env`:
| Variable | Description | Default | | Variable | Description | Default |
|----------|-------------|---------| |----------|-------------|---------|
| `DISCORD_TOKEN` | Your Discord bot token | Required | | `DISCORD_TOKEN` | Your Discord bot token | Required |
| `CHANNEL_ID` | Discord channel ID for updates | Required | | `CHANNEL_ID` | Discord channel ID(s) for updates (comma-separated for multiple) | Required |
| `COMMAND_PREFIX` | Command prefix for bot | `!` | | `COMMAND_PREFIX` | Command prefix for bot | `!` |
| `PRIMARY_TICKER` | Stock ticker for hourly updates | `PYPL` | | `PRIMARY_TICKER` | Stock ticker for hourly updates | `PYPL` |
| `UPDATE_INTERVAL_HOURS` | Hours between updates | `1` | | `UPDATE_INTERVAL_HOURS` | Hours between updates | `1` |
**Multi-Channel Support:** To send updates to multiple channels, provide comma-separated channel IDs:
```
CHANNEL_ID=1442203998932304035,9876543210123456789
```
## Switching Stock API Providers ## Switching Stock API Providers
The bot is designed with an abstract API layer. To switch providers: The bot is designed with an abstract API layer. To switch providers:

23
bot.py
View File

@@ -36,7 +36,7 @@ class StockBot(commands.Bot):
logger.info("Using Yahoo Finance API for stock data") logger.info("Using Yahoo Finance API for stock data")
self.scheduler = AsyncIOScheduler(timezone=pytz.timezone('America/New_York')) self.scheduler = AsyncIOScheduler(timezone=pytz.timezone('America/New_York'))
self.target_channel_id = int(Config.CHANNEL_ID) self.target_channel_ids = [int(id) for id in Config.CHANNEL_IDS]
self.primary_ticker = Config.PRIMARY_TICKER self.primary_ticker = Config.PRIMARY_TICKER
async def setup_hook(self): async def setup_hook(self):
@@ -67,14 +67,15 @@ class StockBot(commands.Bot):
"""Called when bot successfully connects to Discord.""" """Called when bot successfully connects to Discord."""
logger.info(f'Bot connected as {self.user.name} (ID: {self.user.id})') logger.info(f'Bot connected as {self.user.name} (ID: {self.user.id})')
logger.info(f'Monitoring ticker: {self.primary_ticker}') logger.info(f'Monitoring ticker: {self.primary_ticker}')
logger.info(f'Target channel ID: {self.target_channel_id}') logger.info(f'Target channel IDs: {self.target_channel_ids}')
# Verify channel exists # Verify channels exist
channel = self.get_channel(self.target_channel_id) for channel_id in self.target_channel_ids:
if channel: channel = self.get_channel(channel_id)
logger.info(f'Target channel found: #{channel.name}') if channel:
else: logger.info(f'Target channel found: #{channel.name} (ID: {channel_id})')
logger.error(f'Could not find channel with ID {self.target_channel_id}') else:
logger.error(f'Could not find channel with ID {channel_id}')
async def send_hourly_update(self): async def send_hourly_update(self):
"""Send hourly stock price update if market is open.""" """Send hourly stock price update if market is open."""
@@ -83,7 +84,8 @@ class StockBot(commands.Bot):
return return
logger.info(f"Sending hourly update for {self.primary_ticker}") logger.info(f"Sending hourly update for {self.primary_ticker}")
await self.send_stock_update(self.primary_ticker, self.target_channel_id) for channel_id in self.target_channel_ids:
await self.send_stock_update(self.primary_ticker, channel_id)
async def send_market_close_update(self): async def send_market_close_update(self):
"""Send stock price update at market close on trading days.""" """Send stock price update at market close on trading days."""
@@ -92,7 +94,8 @@ class StockBot(commands.Bot):
return return
logger.info(f"Sending market close update for {self.primary_ticker}") logger.info(f"Sending market close update for {self.primary_ticker}")
await self.send_stock_update(self.primary_ticker, self.target_channel_id) for channel_id in self.target_channel_ids:
await self.send_stock_update(self.primary_ticker, channel_id)
async def send_stock_update(self, ticker: str, channel_id: int): async def send_stock_update(self, ticker: str, channel_id: int):
""" """

View File

@@ -10,6 +10,7 @@ class Config:
# Discord configuration # Discord configuration
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
CHANNEL_ID = os.getenv('CHANNEL_ID') CHANNEL_ID = os.getenv('CHANNEL_ID')
CHANNEL_IDS = [id.strip() for id in os.getenv('CHANNEL_ID', '').split(',') if id.strip()]
COMMAND_PREFIX = os.getenv('COMMAND_PREFIX', '!') COMMAND_PREFIX = os.getenv('COMMAND_PREFIX', '!')
# Stock configuration # Stock configuration
@@ -36,10 +37,15 @@ class Config:
print("ERROR: CHANNEL_ID not set in environment") print("ERROR: CHANNEL_ID not set in environment")
return False return False
if not cls.CHANNEL_IDS:
print("ERROR: No valid channel IDs found in CHANNEL_ID")
return False
try: try:
int(cls.CHANNEL_ID) for channel_id in cls.CHANNEL_IDS:
int(channel_id)
except ValueError: except ValueError:
print("ERROR: CHANNEL_ID must be a numeric Discord channel ID") print("ERROR: CHANNEL_ID must contain numeric Discord channel IDs (comma-separated)")
return False return False
if cls.STOCK_API_PROVIDER == 'finnhub' and not cls.FINNHUB_API_KEY: if cls.STOCK_API_PROVIDER == 'finnhub' and not cls.FINNHUB_API_KEY: