kami_dev #1
|
@ -5,10 +5,10 @@ import importlib
|
||||||
import cmd_discord
|
import cmd_discord
|
||||||
|
|
||||||
class DiscordBot(commands.Bot):
|
class DiscordBot(commands.Bot):
|
||||||
def __init__(self, config, logger):
|
def __init__(self, config, log_func):
|
||||||
super().__init__(command_prefix="!", intents=discord.Intents.all())
|
super().__init__(command_prefix="!", intents=discord.Intents.all())
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = logger
|
self.log = log_func # Use the logging function from bots.py
|
||||||
self.load_commands()
|
self.load_commands()
|
||||||
|
|
||||||
def load_commands(self):
|
def load_commands(self):
|
||||||
|
@ -18,21 +18,15 @@ class DiscordBot(commands.Bot):
|
||||||
try:
|
try:
|
||||||
importlib.reload(cmd_discord)
|
importlib.reload(cmd_discord)
|
||||||
cmd_discord.setup(self)
|
cmd_discord.setup(self)
|
||||||
self.logger.info("Discord commands loaded successfully.")
|
self.log("Discord commands loaded successfully.", "INFO")
|
||||||
except Exception as e:
|
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):
|
async def on_ready(self):
|
||||||
self.logger.info(f"Discord bot is online as {self.user}")
|
self.log(f"Discord bot is online as {self.user}", "INFO")
|
||||||
|
|
||||||
# @commands.command()
|
|
||||||
# async def reload(self, ctx):
|
|
||||||
# """ Reload all Discord commands dynamically. """
|
|
||||||
# self.load_commands()
|
|
||||||
# await ctx.send("Commands reloaded!")
|
|
||||||
|
|
||||||
async def run(self, token):
|
async def run(self, token):
|
||||||
try:
|
try:
|
||||||
await super().start(token)
|
await super().start(token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Discord bot error: {e}")
|
self.log(f"Discord bot error: {e}", "CRITICAL")
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
|
# bot_twitch.py
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import logging
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from twitchio.ext import commands
|
from twitchio.ext import commands
|
||||||
import importlib
|
import importlib
|
||||||
import cmd_twitch
|
import cmd_twitch
|
||||||
|
|
||||||
class TwitchBot(commands.Bot):
|
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_id = os.getenv("TWITCH_CLIENT_ID")
|
||||||
self.client_secret = os.getenv("TWITCH_CLIENT_SECRET")
|
self.client_secret = os.getenv("TWITCH_CLIENT_SECRET")
|
||||||
self.token = os.getenv("TWITCH_BOT_TOKEN")
|
self.token = os.getenv("TWITCH_BOT_TOKEN")
|
||||||
self.refresh_token = os.getenv("TWITCH_REFRESH_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
|
self.config = config
|
||||||
|
|
||||||
# 1) Initialize the parent Bot FIRST
|
# 1) Initialize the parent Bot FIRST
|
||||||
|
@ -30,7 +30,7 @@ class TwitchBot(commands.Bot):
|
||||||
Refreshes the Twitch access token using the stored refresh token.
|
Refreshes the Twitch access token using the stored refresh token.
|
||||||
Retries up to 2 times before logging a fatal error.
|
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"
|
url = "https://id.twitch.tv/oauth2/token"
|
||||||
params = {
|
params = {
|
||||||
|
@ -53,19 +53,18 @@ class TwitchBot(commands.Bot):
|
||||||
os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token
|
os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token
|
||||||
self.update_env_file()
|
self.update_env_file()
|
||||||
|
|
||||||
self.logger.info("Twitch token refreshed successfully.")
|
self.log("Twitch token refreshed successfully.", "INFO")
|
||||||
return # Success, exit function
|
return # Success, exit function
|
||||||
else:
|
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:
|
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
|
await asyncio.sleep(10) # Wait before retrying
|
||||||
|
|
||||||
# If all attempts fail, log error
|
# If all attempts fail, log error
|
||||||
from bots import log_error
|
self.log("Twitch token refresh failed after 3 attempts.", "FATAL")
|
||||||
log_error("Twitch token refresh failed after 3 attempts.")
|
|
||||||
|
|
||||||
def update_env_file(self):
|
def update_env_file(self):
|
||||||
"""
|
"""
|
||||||
|
@ -84,10 +83,10 @@ class TwitchBot(commands.Bot):
|
||||||
else:
|
else:
|
||||||
file.write(line)
|
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:
|
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):
|
def load_commands(self):
|
||||||
"""
|
"""
|
||||||
|
@ -96,20 +95,25 @@ class TwitchBot(commands.Bot):
|
||||||
try:
|
try:
|
||||||
importlib.reload(cmd_twitch)
|
importlib.reload(cmd_twitch)
|
||||||
cmd_twitch.setup(self)
|
cmd_twitch.setup(self)
|
||||||
self.logger.info("Twitch commands loaded successfully.")
|
self.log("Twitch commands loaded successfully.", "INFO")
|
||||||
except Exception as e:
|
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):
|
async def run(self):
|
||||||
"""
|
"""
|
||||||
Run the Twitch bot, refreshing tokens if needed.
|
Run the Twitch bot, refreshing tokens if needed.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self.connect() # Connect to Twitch
|
self.log(f"Twitch bot connecting...", "INFO")
|
||||||
await self.start() # Start the bot event loop
|
self.log(f"...Consider online if no further messages", "INFO")
|
||||||
while True:
|
await self.start()
|
||||||
await self.refresh_access_token()
|
#while True:
|
||||||
await asyncio.sleep(10800) # 3 hours
|
# await self.refresh_access_token()
|
||||||
|
# await asyncio.sleep(10800) # Refresh every 3 hours
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from bots import log_error
|
self.log(f"Twitch bot failed to start: {e}", "CRITICAL")
|
||||||
log_error(f"Twitch bot failed to start: {e}")
|
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")
|
||||||
|
|
106
bots.py
106
bots.py
|
@ -2,11 +2,9 @@
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
import time
|
import time
|
||||||
from collections import Counter
|
import traceback
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from bot_discord import DiscordBot
|
from bot_discord import DiscordBot
|
||||||
|
@ -28,95 +26,51 @@ except json.JSONDecodeError as e:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
ERROR_LOG_FILE = "error_log.txt"
|
bot_start_time = time.time()
|
||||||
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")
|
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# 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.
|
A simple logging function with adjustable log levels.
|
||||||
If it fails, logs to error_log.txt instead.
|
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:
|
if level in config_data["log_levels"]:
|
||||||
return # No errors to send
|
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
log_message = f"[{timestamp}] [{level}] {message}"
|
||||||
# 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:
|
|
||||||
try:
|
try:
|
||||||
discord_bot
|
print(log_message) # Print to terminal
|
||||||
except NameError:
|
except Exception:
|
||||||
discord_bot = DiscordBot(config_data, logger)
|
pass # Prevent logging failures from crashing the bot
|
||||||
asyncio.create_task(send_discord_error_report(discord_bot))
|
|
||||||
|
# Placeholder for future expansions (e.g., file logging, Discord alerts, etc.)
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
# Main Event Loop
|
# Main Event Loop
|
||||||
###############################
|
###############################
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
bot_start_time = time.time()
|
|
||||||
global discord_bot, twitch_bot
|
global discord_bot, twitch_bot
|
||||||
|
|
||||||
discord_bot = DiscordBot(config_data, logger)
|
log("Initializing bots...", "INFO")
|
||||||
twitch_bot = TwitchBot(config_data, logger)
|
|
||||||
|
|
||||||
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())
|
twitch_task = asyncio.create_task(twitch_bot.run())
|
||||||
|
|
||||||
await asyncio.gather(discord_task, twitch_task)
|
await asyncio.gather(discord_task, twitch_task)
|
||||||
|
@ -126,4 +80,4 @@ if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_trace = traceback.format_exc()
|
error_trace = traceback.format_exc()
|
||||||
log_error(f"Fatal Error: {e}\n{error_trace}")
|
log(f"Fatal Error: {e}\n{error_trace}", "FATAL")
|
||||||
|
|
|
@ -27,9 +27,38 @@ def ping():
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
elapsed = current_time - bot_start_time
|
elapsed = current_time - bot_start_time
|
||||||
uptime_str = format_uptime(elapsed)
|
uptime_str, uptime_s = format_uptime(elapsed)
|
||||||
response = f"Pong! I've been awake for {uptime_str}. Send coffee!"
|
# Define thresholds in ascending order
|
||||||
return f"{response}"
|
# (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:
|
def greet(target_display_name: str, platform_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"discord_guilds": [896713616089309184],
|
"discord_guilds": [896713616089309184],
|
||||||
"twitch_channels": ["OokamiKunTV", "ookamipup"],
|
"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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +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_BOT_TOKEN=[Discord bot/access token]
|
||||||
DISCORD_OWNER_ID=[Discord owner user ID]
|
DISCORD_OWNER_ID=[Discord owner user ID]
|
||||||
|
|
||||||
|
# Twitch bot credentials
|
||||||
TWITCH_CLIENT_ID=[Twitch bot client ID]
|
TWITCH_CLIENT_ID=[Twitch bot client ID]
|
||||||
TWITCH_CLIENT_SECRET=[Twitch bot client secret]
|
TWITCH_CLIENT_SECRET=[Twitch bot client secret]
|
||||||
TWITCH_BOT_TOKEN=[Twitch bot/access token]
|
TWITCH_BOT_TOKEN=[Twitch bot/access token]
|
||||||
TWITCH_REFRESH_TOKEN=[Twitch bot refresh token]
|
TWITCH_REFRESH_TOKEN=[Twitch bot refresh token]
|
||||||
|
|
||||||
|
# MariaDB Database credentials
|
||||||
DB_HOST=[Database host IP address]
|
DB_HOST=[Database host IP address]
|
||||||
DB_USER=[Database username]
|
DB_USER=[Database username]
|
||||||
DB_PASSWORD=[Database user password]
|
DB_PASSWORD=[Database user password]
|
||||||
DB_NAME=[Database name]
|
DB_NAME=[Database name]
|
||||||
DB_PORT=[Database port]
|
DB_PORT=[Database port]
|
|
@ -21,14 +21,14 @@ def format_uptime(seconds: float) -> str:
|
||||||
# Build a short string, only listing the largest units
|
# Build a short string, only listing the largest units
|
||||||
# If you want more detail, you can keep going
|
# If you want more detail, you can keep going
|
||||||
if years > 0:
|
if years > 0:
|
||||||
return f"{years} year(s), {months} month(s)"
|
return [f"{years} year(s), {months} month(s)", seconds]
|
||||||
elif months > 0:
|
elif months > 0:
|
||||||
return f"{months} month(s), {days} day(s)"
|
return [f"{months} month(s), {days} day(s)", seconds]
|
||||||
elif days > 0:
|
elif days > 0:
|
||||||
return f"{days} day(s), {hours} hour(s)"
|
return [f"{days} day(s), {hours} hour(s)", seconds]
|
||||||
elif hours > 0:
|
elif hours > 0:
|
||||||
return f"{hours} hour(s), {minutes} minute(s)"
|
return [f"{hours} hour(s), {minutes} minute(s)", seconds]
|
||||||
elif minutes > 0:
|
elif minutes > 0:
|
||||||
return f"{minutes} minute(s)"
|
return [f"{minutes} minute(s)", seconds]
|
||||||
else:
|
else:
|
||||||
return f"{seconds} second(s)"
|
return [f"{seconds} second(s)", seconds]
|
|
@ -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
|
|
@ -1,4 +1,13 @@
|
||||||
discord.py
|
# Core Dependencies
|
||||||
twitchio
|
python-dotenv==1.0.0 # Loads environment variables from .env
|
||||||
python-dotenv
|
requests==2.31.0 # HTTP requests for Twitch API (token refresh, etc.)
|
||||||
PyMySQL
|
|
||||||
|
# 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)
|
||||||
|
|
Loading…
Reference in New Issue