From 2e33792aa8c7947e770549677d53ffbaf0e2d62d Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 13:04:07 +0100 Subject: [PATCH 1/6] Added ping awake replies --- cmd_common/common_commands.py | 35 ++++++++++++++++++++++++++++++++--- modules/utility.py | 12 ++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/cmd_common/common_commands.py b/cmd_common/common_commands.py index b954e99..87c52dc 100644 --- a/cmd_common/common_commands.py +++ b/cmd_common/common_commands.py @@ -27,9 +27,38 @@ def ping(): current_time = time.time() elapsed = current_time - bot_start_time - uptime_str = format_uptime(elapsed) - response = f"Pong! I've been awake for {uptime_str}. Send coffee!" - return f"{response}" + uptime_str, uptime_s = format_uptime(elapsed) + # Define thresholds in ascending order + # (threshold_in_seconds, message) + # The message is a short "desperation" or "awake" text. + time_ranges = [ + (3600, f"I've been awake for {uptime_str}. I just woke up, feeling great!"), # < 1 hour + (10800, f"I've been awake for {uptime_str}. I'm still fairly fresh!"), # 3 hours + (21600, f"I've been awake for {uptime_str}. I'm starting to get a bit weary..."), # 6 hours + (43200, f"I've been awake for {uptime_str}. 12 hours?! Might be time for coffee."), # 12 hours + (86400, f"I've been awake for {uptime_str}. A whole day without sleep... I'm okay?"), # 1 day + (172800, f"I've been awake for {uptime_str}. Two days... I'd love a nap."), # 2 days + (259200, f"I've been awake for {uptime_str}. Three days. Is sleep optional now?"), # 3 days + (345600, f"I've been awake for {uptime_str}. Four days... I'm running on fumes."), # 4 days + (432000, f"I've been awake for {uptime_str}. Five days. Please send more coffee."), # 5 days + (518400, f"I've been awake for {uptime_str}. Six days. I've forgotten what dreams are."), # 6 days + (604800, f"I've been awake for {uptime_str}. One week. I'm turning into a zombie."), # 7 days + (1209600, f"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?"), # 14 days + (2592000, f"I've been awake for {uptime_str}. A month! The nightmares never end."), # 30 days + (7776000, f"I've been awake for {uptime_str}. Three months. I'm mostly coffee now."), # 90 days + (15552000,f"I've been awake for {uptime_str}. Six months. This is insane..."), # 180 days + (23328000,f"I've been awake for {uptime_str}. Nine months. I might be unstoppable."), # 270 days + (31536000,f"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia..."), # 365 days + ] + + # We'll iterate from smallest to largest threshold + for threshold, msg in time_ranges: + if uptime_s < threshold: + return msg + + # If none matched, it means uptime_s >= 31536000 (1 year+) + return f"I've been awake for {uptime_str}. Over a year awake... I'm beyond mortal limits!" + def greet(target_display_name: str, platform_name: str) -> str: """ diff --git a/modules/utility.py b/modules/utility.py index bec6ebd..eab6097 100644 --- a/modules/utility.py +++ b/modules/utility.py @@ -21,14 +21,14 @@ def format_uptime(seconds: float) -> str: # Build a short string, only listing the largest units # If you want more detail, you can keep going if years > 0: - return f"{years} year(s), {months} month(s)" + return [f"{years} year(s), {months} month(s)", seconds] elif months > 0: - return f"{months} month(s), {days} day(s)" + return [f"{months} month(s), {days} day(s)", seconds] elif days > 0: - return f"{days} day(s), {hours} hour(s)" + return [f"{days} day(s), {hours} hour(s)", seconds] elif hours > 0: - return f"{hours} hour(s), {minutes} minute(s)" + return [f"{hours} hour(s), {minutes} minute(s)", seconds] elif minutes > 0: - return f"{minutes} minute(s)" + return [f"{minutes} minute(s)", seconds] else: - return f"{seconds} second(s)" \ No newline at end of file + return [f"{seconds} second(s)", seconds] \ No newline at end of file From 2192637d91beb4234ec46c4bb445986cee09115c Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 13:09:02 +0100 Subject: [PATCH 2/6] Fixed unavailable bot_start_time variable for !ping command --- bots.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bots.py b/bots.py index b05470e..e7d21e1 100644 --- a/bots.py +++ b/bots.py @@ -109,8 +109,9 @@ def log_error(error_msg): # Main Event Loop ############################### +global bot_start_time +bot_start_time = time.time() async def main(): - bot_start_time = time.time() global discord_bot, twitch_bot discord_bot = DiscordBot(config_data, logger) From 9727927b66e39d9d66f6dbda89e6a7ac68e1a4ae Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 13:13:17 +0100 Subject: [PATCH 3/6] Stopped tracking .env file --- .env | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index d2f4faf..0000000 --- a/.env +++ /dev/null @@ -1,13 +0,0 @@ -DISCORD_BOT_TOKEN=[Discord bot/access token] -DISCORD_OWNER_ID=[Discord owner user ID] - -TWITCH_CLIENT_ID=[Twitch bot client ID] -TWITCH_CLIENT_SECRET=[Twitch bot client secret] -TWITCH_BOT_TOKEN=[Twitch bot/access token] -TWITCH_REFRESH_TOKEN=[Twitch bot refresh token] - -DB_HOST=[Database host IP address] -DB_USER=[Database username] -DB_PASSWORD=[Database user password] -DB_NAME=[Database name] -DB_PORT=[Database port] From 4c1ce0e2505b9e6efaa4cebdb5059a4590c42886 Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 1 Feb 2025 12:16:51 +0000 Subject: [PATCH 4/6] Add example.env Added example .env file --- example.env | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 example.env diff --git a/example.env b/example.env new file mode 100644 index 0000000..4c03f9c --- /dev/null +++ b/example.env @@ -0,0 +1,19 @@ +# Example .env file. +# Rename this file to ".env" and fill in your credentials accordingly. + +# Discord bot credentials +DISCORD_BOT_TOKEN=[Discord bot/access token] +DISCORD_OWNER_ID=[Discord owner user ID] + +# Twitch bot credentials +TWITCH_CLIENT_ID=[Twitch bot client ID] +TWITCH_CLIENT_SECRET=[Twitch bot client secret] +TWITCH_BOT_TOKEN=[Twitch bot/access token] +TWITCH_REFRESH_TOKEN=[Twitch bot refresh token] + +# MariaDB Database credentials +DB_HOST=[Database host IP address] +DB_USER=[Database username] +DB_PASSWORD=[Database user password] +DB_NAME=[Database name] +DB_PORT=[Database port] \ No newline at end of file From 82aec3dc5ff5f42c431ce92a2e5e4ab5a1038026 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 20:50:54 +0100 Subject: [PATCH 5/6] - Reverted logging to a simpler terminal-only - Added log levels in config.json - Reworked requirements.txt --- bot_discord.py | 18 ++---- bot_twitch.py | 44 +++++++------- bots.py | 107 ++++++++++------------------------ cmd_common/common_commands.py | 14 ++--- config.json | 3 +- requirements.txt | 17 ++++-- 6 files changed, 82 insertions(+), 121 deletions(-) diff --git a/bot_discord.py b/bot_discord.py index 0bdb601..4365f2d 100644 --- a/bot_discord.py +++ b/bot_discord.py @@ -5,10 +5,10 @@ import importlib import cmd_discord class DiscordBot(commands.Bot): - def __init__(self, config, logger): + def __init__(self, config, log_func): super().__init__(command_prefix="!", intents=discord.Intents.all()) self.config = config - self.logger = logger + self.log = log_func # Use the logging function from bots.py self.load_commands() def load_commands(self): @@ -18,21 +18,15 @@ class DiscordBot(commands.Bot): try: importlib.reload(cmd_discord) cmd_discord.setup(self) - self.logger.info("Discord commands loaded successfully.") + self.log("Discord commands loaded successfully.", "INFO") except Exception as e: - self.logger.error(f"Error loading Discord commands: {e}") + self.log(f"Error loading Discord commands: {e}", "ERROR") async def on_ready(self): - self.logger.info(f"Discord bot is online as {self.user}") - -# @commands.command() -# async def reload(self, ctx): -# """ Reload all Discord commands dynamically. """ -# self.load_commands() -# await ctx.send("Commands reloaded!") + self.log(f"Discord bot is online as {self.user}", "INFO") async def run(self, token): try: await super().start(token) except Exception as e: - self.logger.error(f"Discord bot error: {e}") + self.log(f"Discord bot error: {e}", "CRITICAL") diff --git a/bot_twitch.py b/bot_twitch.py index 8b06a51..fc24402 100644 --- a/bot_twitch.py +++ b/bot_twitch.py @@ -1,18 +1,18 @@ +# bot_twitch.py import os import requests -import logging import asyncio from twitchio.ext import commands import importlib import cmd_twitch class TwitchBot(commands.Bot): - def __init__(self, config, logger): + def __init__(self, config, log_func): self.client_id = os.getenv("TWITCH_CLIENT_ID") self.client_secret = os.getenv("TWITCH_CLIENT_SECRET") self.token = os.getenv("TWITCH_BOT_TOKEN") self.refresh_token = os.getenv("TWITCH_REFRESH_TOKEN") - self.logger = logger + self.log = log_func # Use the logging function from bots.py self.config = config # 1) Initialize the parent Bot FIRST @@ -30,7 +30,7 @@ class TwitchBot(commands.Bot): Refreshes the Twitch access token using the stored refresh token. Retries up to 2 times before logging a fatal error. """ - self.logger.info("Attempting to refresh Twitch token...") + self.log("Attempting to refresh Twitch token...", "INFO") url = "https://id.twitch.tv/oauth2/token" params = { @@ -53,19 +53,18 @@ class TwitchBot(commands.Bot): os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token self.update_env_file() - self.logger.info("Twitch token refreshed successfully.") + self.log("Twitch token refreshed successfully.", "INFO") return # Success, exit function else: - self.logger.warning(f"Twitch token refresh failed (Attempt {attempt+1}/3): {data}") + self.log(f"Twitch token refresh failed (Attempt {attempt+1}/3): {data}", "WARNING") except Exception as e: - self.logger.error(f"Twitch token refresh error (Attempt {attempt+1}/3): {e}") + self.log(f"Twitch token refresh error (Attempt {attempt+1}/3): {e}", "ERROR") await asyncio.sleep(10) # Wait before retrying # If all attempts fail, log error - from bots import log_error - log_error("Twitch token refresh failed after 3 attempts.") + self.log("Twitch token refresh failed after 3 attempts.", "FATAL") def update_env_file(self): """ @@ -84,10 +83,10 @@ class TwitchBot(commands.Bot): else: file.write(line) - self.logger.info("Updated .env file with new Twitch token.") + self.log("Updated .env file with new Twitch token.", "INFO") except Exception as e: - self.logger.error(f"Failed to update .env file: {e}") + self.log(f"Failed to update .env file: {e}", "ERROR") def load_commands(self): """ @@ -96,20 +95,25 @@ class TwitchBot(commands.Bot): try: importlib.reload(cmd_twitch) cmd_twitch.setup(self) - self.logger.info("Twitch commands loaded successfully.") + self.log("Twitch commands loaded successfully.", "INFO") except Exception as e: - self.logger.error(f"Error loading Twitch commands: {e}") + self.log(f"Error loading Twitch commands: {e}", "ERROR") async def run(self): """ Run the Twitch bot, refreshing tokens if needed. """ try: - await self.connect() # Connect to Twitch - await self.start() # Start the bot event loop - while True: - await self.refresh_access_token() - await asyncio.sleep(10800) # 3 hours + self.log(f"Twitch bot connecting...", "INFO") + self.log(f"...Consider online if no further messages", "INFO") + await self.start() + #while True: + # await self.refresh_access_token() + # await asyncio.sleep(10800) # Refresh every 3 hours except Exception as e: - from bots import log_error - log_error(f"Twitch bot failed to start: {e}") + self.log(f"Twitch bot failed to start: {e}", "CRITICAL") + if "Invalid or unauthorized Access Token passed." in str(e): + try: + await self.refresh_access_token() + except Exception as e: + self.log(f"Unable to refresh Twitch token! Twitch bot will be offline!", "CRITICAL") diff --git a/bots.py b/bots.py index e7d21e1..aa50377 100644 --- a/bots.py +++ b/bots.py @@ -2,11 +2,9 @@ import os import json import asyncio -import logging import sys -import traceback import time -from collections import Counter +import traceback from discord.ext import commands from dotenv import load_dotenv from bot_discord import DiscordBot @@ -28,96 +26,51 @@ except json.JSONDecodeError as e: sys.exit(1) # Global settings -ERROR_LOG_FILE = "error_log.txt" -DISCORD_USER_ID = os.getenv("DISCORD_OWNER_ID") # Your Discord user ID for DM alerts -DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") # Discord bot token -ERROR_CACHE = [] # Stores pending errors -LAST_ERROR_SENT = 0 # Timestamp of last Discord error message -ERROR_COOLDOWN = 900 # 15 minutes (900 sec) cooldown between Discord reports -ERROR_BATCH_TIMEOUT = 5 # 5 seconds batching delay before sending - -# Setup logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - stream=sys.stdout -) -logger = logging.getLogger("Main") +bot_start_time = time.time() ############################### -# Error Handling & Discord Alerts +# Simple Logging System ############################### -async def send_discord_error_report(bot): +def log(message, level="INFO"): """ - Sends cached errors as a Discord DM with cooldowns and batching. - If it fails, logs to error_log.txt instead. + A simple logging function with adjustable log levels. + Logs messages in a structured format. + + Available levels: + DEBUG, INFO, WARNING, ERROR, CRITICAL, FATAL + See 'config.json' for disabling/enabling logging levels """ - global ERROR_CACHE, LAST_ERROR_SENT + log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] + if level not in log_levels: + level = "INFO" # Default to INFO if an invalid level is provided - if not ERROR_CACHE: - return # No errors to send - - # Wait for batching timeout - await asyncio.sleep(ERROR_BATCH_TIMEOUT) - - # Compress duplicate errors - error_counter = Counter(ERROR_CACHE) - compressed_errors = [f"{err} (Occurred {count} times)" if count > 1 else err for err, count in error_counter.items()] - error_message = "\n".join(compressed_errors) - - # Ensure message fits in Discord limits - if len(error_message) > 2000: - error_message = error_message[:1990] + "\n... (truncated)" - - # Update timestamp - LAST_ERROR_SENT = time.time() - - try: - user = await bot.fetch_user(DISCORD_USER_ID) - await user.send(f"**Bot Error Report**\n\n{error_message}") - logger.info(f"[ERROR REPORT] Sent {len(ERROR_CACHE)} errors at {time.strftime('%H:%M:%S')} | Discord Send Status: Success") - except Exception as e: - # If Discord DM fails, log errors to a file - with open(ERROR_LOG_FILE, "a") as f: - timestamp = time.strftime('%Y-%m-%d %H:%M:%S') - f.write(f"\n----- {len(ERROR_CACHE)} Errors Occurred at {timestamp} -----\n") - for err in ERROR_CACHE: - f.write(f"{err}\n") - - logger.error(f"[ERROR REPORT] Failed to send Discord DM. Errors saved to {ERROR_LOG_FILE}") - - # Clear cache after sending - ERROR_CACHE = [] - -def log_error(error_msg): - """ - Handles error logging with cooldowns. - If multiple errors occur in a short time, they are cached and sent in batch. - """ - global ERROR_CACHE, LAST_ERROR_SENT - - ERROR_CACHE.append(error_msg) - if time.time() - LAST_ERROR_SENT >= ERROR_COOLDOWN: + if level in config_data["log_levels"]: + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + log_message = f"[{timestamp}] [{level}] {message}" + try: - discord_bot - except NameError: - discord_bot = DiscordBot(config_data, logger) - asyncio.create_task(send_discord_error_report(discord_bot)) + print(log_message) # Print to terminal + except Exception: + pass # Prevent logging failures from crashing the bot + + # Placeholder for future expansions (e.g., file logging, Discord alerts, etc.) ############################### # Main Event Loop ############################### -global bot_start_time -bot_start_time = time.time() async def main(): global discord_bot, twitch_bot - discord_bot = DiscordBot(config_data, logger) - twitch_bot = TwitchBot(config_data, logger) + log("Initializing bots...", "INFO") - discord_task = asyncio.create_task(discord_bot.run(DISCORD_BOT_TOKEN)) + discord_bot = DiscordBot(config_data, log) + twitch_bot = TwitchBot(config_data, log) + + log("Starting Discord and Twitch bots...", "INFO") + + discord_task = asyncio.create_task(discord_bot.run(os.getenv("DISCORD_BOT_TOKEN"))) twitch_task = asyncio.create_task(twitch_bot.run()) await asyncio.gather(discord_task, twitch_task) @@ -127,4 +80,4 @@ if __name__ == "__main__": asyncio.run(main()) except Exception as e: error_trace = traceback.format_exc() - log_error(f"Fatal Error: {e}\n{error_trace}") + log(f"Fatal Error: {e}\n{error_trace}", "FATAL") diff --git a/cmd_common/common_commands.py b/cmd_common/common_commands.py index 87c52dc..b49bba9 100644 --- a/cmd_common/common_commands.py +++ b/cmd_common/common_commands.py @@ -42,13 +42,13 @@ def ping(): (345600, f"I've been awake for {uptime_str}. Four days... I'm running on fumes."), # 4 days (432000, f"I've been awake for {uptime_str}. Five days. Please send more coffee."), # 5 days (518400, f"I've been awake for {uptime_str}. Six days. I've forgotten what dreams are."), # 6 days - (604800, f"I've been awake for {uptime_str}. One week. I'm turning into a zombie."), # 7 days - (1209600, f"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?"), # 14 days - (2592000, f"I've been awake for {uptime_str}. A month! The nightmares never end."), # 30 days - (7776000, f"I've been awake for {uptime_str}. Three months. I'm mostly coffee now."), # 90 days - (15552000,f"I've been awake for {uptime_str}. Six months. This is insane..."), # 180 days - (23328000,f"I've been awake for {uptime_str}. Nine months. I might be unstoppable."), # 270 days - (31536000,f"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia..."), # 365 days + (604800, f"I've been awake for {uptime_str}. One week. I'm turning into a zombie."), # 7 days + (1209600, f"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?"), # 14 days + (2592000, f"I've been awake for {uptime_str}. A month! The nightmares never end."), # 30 days + (7776000, f"I've been awake for {uptime_str}. Three months. I'm mostly coffee now."), # 90 days + (15552000,f"I've been awake for {uptime_str}. Six months. This is insane..."), # 180 days + (23328000,f"I've been awake for {uptime_str}. Nine months. I might be unstoppable."), # 270 days + (31536000,f"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia..."), # 365 days ] # We'll iterate from smallest to largest threshold diff --git a/config.json b/config.json index 15b590d..47e080b 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,7 @@ { "discord_guilds": [896713616089309184], "twitch_channels": ["OokamiKunTV", "ookamipup"], - "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"] + "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"], + "log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 688004a..9049826 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,13 @@ -discord.py -twitchio -python-dotenv -PyMySQL +# Core Dependencies +python-dotenv==1.0.0 # Loads environment variables from .env +requests==2.31.0 # HTTP requests for Twitch API (token refresh, etc.) + +# Discord Bot Dependencies +discord.py==2.3.2 # Main Discord bot library (async) +PyNaCl==1.5.0 # Enables voice support (optional, required if using voice features) + +# Twitch Bot Dependencies +twitchio==2.7.1 # Twitch chat bot library (async) + +# Utility & Logging +aiohttp==3.9.1 # Async HTTP requests (dependency for discord.py & twitchio) From 2a977fe76e08aa2c7ccc71be745c0286ee9245d9 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 21:28:26 +0100 Subject: [PATCH 6/6] - Added readme.md --- readme.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..66eb7bb --- /dev/null +++ b/readme.md @@ -0,0 +1,152 @@ +# OokamiPup V2 + +## A combined Discord and Twitch bot written in Python, leveraging: + +- discord.py (PyPI version discord.py or a maintained fork) +- TwitchIO +- aiohttp +- python-dotenv +- and other libraries listed in your requirements.txt. + +## About the bot + +- Monitors specified Twitch channels for messages and commands. +- Connects to a specified Discord server to respond to commands and events. +- Uses a .env file for storing sensitive credentials. +- Uses a config.json for channel configuration, logging settings, etc. + +## Features + +- Discord Bot + - Responds to commands from any channel or restricted channels (depending on your setup). + - Automatically loads commands from cmd_discord.py. +- Twitch Bot + - Joins multiple Twitch channels specified in your config.json. + - Automatically loads commands from cmd_twitch.py. +- Simple Logging + - Logs to the console with optional levels (DEBUG, INFO, WARNING, etc.). +- Token Refresh + - Automatically attempts to refresh your Twitch OAuth token if it becomes invalid. + +## Prerequisites + +- Python 3.8+ (or a compatible Python 3 version—ensure asyncio.run is supported). +- A Twitch Developer application (for your Client ID and Client Secret). +- A Discord application/bot token. +- A working pip or pipenv/poetry for installing dependencies. + +## Installation + +Clone this repository (or download the source code): + +1. `git clone https://git.ookamikun.tv/kami/OokamiPupV2.git` *(Clone repository)* +2. `cd OokamiPupV2` *(Navigate into the project folder)* +3. `pip install -r requirements.txt` *(Install project dependancies)* + +Create your .env file in the project root (same folder as `bots.py`). It should look like: + +```python +DISCORD_BOT_TOKEN=YourDiscordBotToken +TWITCH_CLIENT_ID=YourTwitchAppClientID +TWITCH_CLIENT_SECRET=YourTwitchAppClientSecret +TWITCH_BOT_TOKEN=YourTwitchOAuthToken +TWITCH_REFRESH_TOKEN=YourTwitchRefreshToken +``` + +Note: Your Twitch OAuth/Refresh tokens typically come from generating them through the Twitch Developer Console. + +Set up your config.json with your preferred channels, logging levels, etc. For example: + +```json +{ + "twitch_channels": ["mychannel", "anotherchannel"], + "log_levels": ["INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] +} +``` + +twitch_channels is a list of channel names for your bot to join on Twitch. +log_levels controls what messages get logged to console (The lowest "DEBUG" level can be added if needed). + +## Directory Overview + +A quick rundown of the important files: + +```bash +. +├── bots.py # Main entry point: initializes/starts both Discord and Twitch bots +├── bot_discord.py # Discord bot implementation (using discord.py) +├── bot_twitch.py # Twitch bot implementation (using TwitchIO) +├── cmd_common/ # Platform-independant command logic +│ └── common_commands.py # Common low-level commands file +├── cmd_discord.py # Command definitions for Discord +├── cmd_twitch.py # Command definitions for Twitch +├── config.json # Bot configuration (channels, logging levels, etc.) +├── .env # Environment variables for tokens (DO NOT commit this) +├── requirements.txt # Python dependencies +└── README.md # You are here +``` + +## Usage + +1. Edit your environment variables in the .env file (tokens, client IDs, secrets, etc.). +2. Edit your config.json to list the Twitch channels and your logging preferences. +3. Run the bot: `python bots.py` +4. Check the console output: + - You should see logs indicating that the Discord bot is online (Discord bot is online as ...). + - You should also see logs about the Twitch bot joining channels (Twitch bot connected) — assuming everything is configured correctly. + +## Testing Commands + +**Discord:** + +Open your Discord server, type your command prefix (e.g., !) + the command name (e.g., !ping). + +**Twitch:** + +In one of the channels specified in twitch_channels, type !ping (or whichever commands you have set up in cmd_twitch.py). + +### If your commands do not respond + +- Confirm the bot has correct roles/permissions in Discord (especially if it needs to read/send messages in specific channels). +- Confirm your Twitch OAuth token is valid. +- Check for logs or errors in the console. + +## Adding or Editing Commands + +**General Commands:** + +low-level commands can be made in `cmd_common/common_commands.py`, alternatively separated into separate files in that folder. + +These are command logic meant to support both platforms, eg. the `!ping` command. + +**Discord:** + +- Open cmd_discord.py and define new commands using the standard @bot.command() decorators from discord.ext.commands. +- Make sure cmd_discord.setup(bot) registers them with your DiscordBot. + +**Twitch:** + +- Open cmd_twitch.py and define new commands using the @bot.command() decorators from twitchio.ext.commands. +- Ensure cmd_twitch.setup(bot) properly registers them with TwitchBot. + +## Troubleshooting + +- No logs from the Twitch bot + - Ensure that you’re calling await bot.start() (not bot.run() and await bot.start() together). + - If you only see the Discord bot logs, verify that your .env variables are loaded and valid (TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET, TWITCH_BOT_TOKEN, TWITCH_REFRESH_TOKEN). + - Confirm that you have internet connectivity and that the Twitch IRC service isn’t blocked by a firewall. + +- Token refresh issues + - If you see repeated warnings about “Invalid or unauthorized Access Token,” double-check your TWITCH_REFRESH_TOKEN and TWITCH_CLIENT_SECRET are correct. + - The bot attempts to refresh automatically; if it still fails, manually re-generate tokens from the Twitch Developer Console. + +- "Cannot write to closing transport" + - Make sure you do not call connect() manually before start() in TwitchIO. + +## Contributing + +- Fork the repository +- Create a new branch (git checkout -b feature/myfeature) +- Make changes and commit (git commit -am 'Add new feature') +- Push to the branch (git push origin feature/myfeature) +- Create a new Pull Request in Git