Added Twitch multi-channel support

- OokamiPup v2 can now enter sevaral community channels
  - Individual channel settings. By default, all commands are disabled
  - Channel-specific settings allow enabling/disabling individual commands per channel
- Added a few more fun facts
- Minor tweaks and bugfixes
kami_dev
Kami 2025-02-16 15:30:17 +01:00
parent 6da1744990
commit 17fbb20cdc
9 changed files with 128 additions and 42 deletions

View File

@ -202,7 +202,12 @@ class TwitchBot(commands.Bot):
async def run(self): async def run(self):
""" """
Run the Twitch bot, refreshing tokens if needed. Run the Twitch bot, refreshing tokens if needed.
""" """
# modules.utility.list_channels(self)
# kami_status = "OokamiKunTV is currently LIVE" if await modules.utility.is_channel_live(self) else "OokamikunTV is currently not streaming"
# globals.log(kami_status)
retries = 0 retries = 0
while True: while True:
if retries > 3: if retries > 3:

View File

@ -7,6 +7,7 @@ import time
import traceback import traceback
import globals import globals
from functools import partial from functools import partial
import twitchio.ext
from discord.ext import commands from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
@ -102,4 +103,4 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
error_trace = traceback.format_exc() error_trace = traceback.format_exc()
globals.log(f"Fatal Error: {e}\n{error_trace}", "FATAL") globals.log(f"Fatal Error: {e}\n{error_trace}", "FATAL")
utility.log_bot_shutdown(db_conn) utility.log_bot_shutdown(db_conn)

View File

@ -6,7 +6,7 @@ import globals
from cmd_common import common_commands as cc from cmd_common import common_commands as cc
from modules.permissions import has_permission from modules.permissions import has_permission
from modules.utility import handle_help_command from modules.utility import handle_help_command, is_channel_live, command_allowed_twitch
def setup(bot, db_conn=None): def setup(bot, db_conn=None):
""" """
@ -14,37 +14,46 @@ def setup(bot, db_conn=None):
We also attach the db_conn and log so the commands can use them. We also attach the db_conn and log so the commands can use them.
""" """
@commands.command(name='funfact', aliases=['fun-fact']) @bot.command(name='funfact', aliases=['fun-fact'])
async def funfact_command(self, ctx: commands.Context, *keywords: str): @command_allowed_twitch
async def cmd_funfact(ctx: commands.Context, *keywords: str):
# Convert keywords tuple to list and pass to get_fun_fact. # Convert keywords tuple to list and pass to get_fun_fact.
fact = cc.get_fun_fact(list(keywords)) fact = cc.get_fun_fact(list(keywords))
# Reply to the invoking user by prefixing their name. # Reply to the invoking user by prefixing their name.
await ctx.reply(f"@{ctx.author.name} {fact}") await ctx.reply(f"@{ctx.author.name} {fact}")
@bot.command(name="greet") @bot.command(name="greet")
@command_allowed_twitch
async def cmd_greet(ctx: commands.Context): async def cmd_greet(ctx: commands.Context):
result = cc.greet(ctx.author.display_name, "Twitch") if not await is_channel_live():
await ctx.reply(result) result = cc.greet(ctx.author.display_name, "Twitch")
await ctx.reply(result)
@bot.command(name="ping") @bot.command(name="ping")
@command_allowed_twitch
async def cmd_ping(ctx: commands.Context): async def cmd_ping(ctx: commands.Context):
result = cc.ping() if not await is_channel_live():
await ctx.reply(result) result = cc.ping()
await ctx.reply(result)
@bot.command(name="howl") @bot.command(name="howl")
@command_allowed_twitch
async def cmd_howl(ctx: commands.Context): async def cmd_howl(ctx: commands.Context):
response = cc.handle_howl_command(ctx) if not await is_channel_live():
await ctx.reply(response) response = cc.handle_howl_command(ctx)
await ctx.reply(response)
@bot.command(name="hi") @bot.command(name="hi")
@command_allowed_twitch
async def cmd_hi(ctx: commands.Context): async def cmd_hi(ctx: commands.Context):
user_id = str(ctx.author.id) # Twitch user ID if not await is_channel_live():
user_roles = [role.lower() for role in ctx.author.badges.keys()] # "roles" from Twitch badges user_id = str(ctx.author.id) # Twitch user ID
user_roles = [role.lower() for role in ctx.author.badges.keys()] # "roles" from Twitch badges
if not has_permission("hi", user_id, user_roles, "twitch"): if not has_permission("hi", user_id, user_roles, "twitch"):
return await ctx.send("You don't have permission to use this command.") return await ctx.send("You don't have permission to use this command.")
await ctx.reply("Hello there!") await ctx.reply("Hello there!")
# @bot.command(name="acc_link") # @bot.command(name="acc_link")
# @monitor_cmds(bot.log) # @monitor_cmds(bot.log)
@ -122,6 +131,7 @@ def setup(bot, db_conn=None):
# await ctx.send(result) # await ctx.send(result)
@bot.command(name="quote") @bot.command(name="quote")
@command_allowed_twitch
async def cmd_quote(ctx: commands.Context): async def cmd_quote(ctx: commands.Context):
""" """
Handles the !quote command with multiple subcommands. Handles the !quote command with multiple subcommands.
@ -144,37 +154,40 @@ def setup(bot, db_conn=None):
- !quote last/latest/newest - !quote last/latest/newest
-> Retrieves the latest (most recent) non-removed quote. -> Retrieves the latest (most recent) non-removed quote.
""" """
if not globals.init_db_conn: if not await is_channel_live():
await ctx.reply("Database is unavailable, sorry.") if not globals.init_db_conn:
return await ctx.reply("Database is unavailable, sorry.")
return
# Parse the arguments from the message text # Parse the arguments from the message text
args = ctx.message.content.strip().split() args = ctx.message.content.strip().split()
args = args[1:] if args else [] args = args[1:] if args else []
globals.log(f"'quote' command initiated with arguments: {args}", "DEBUG") globals.log(f"'quote' command initiated with arguments: {args}", "DEBUG")
globals.log(f"'quote' command message content: {ctx.message.content}", "DEBUG") globals.log(f"'quote' command message content: {ctx.message.content}", "DEBUG")
def get_twitch_game_for_channel(chan_name): def get_twitch_game_for_channel(chan_name):
# Placeholder for your actual logic to fetch the current game # Placeholder for your actual logic to fetch the current game
return "SomeGame" return "SomeGame"
result = await cc.handle_quote_command( result = await cc.handle_quote_command(
db_conn=globals.init_db_conn, db_conn=globals.init_db_conn,
is_discord=False, is_discord=False,
ctx=ctx, ctx=ctx,
args=args, args=args,
get_twitch_game_for_channel=get_twitch_game_for_channel get_twitch_game_for_channel=get_twitch_game_for_channel
) )
globals.log(f"'quote' result: {result}", "DEBUG") globals.log(f"'quote' result: {result}", "DEBUG")
await ctx.reply(result) await ctx.reply(result)
@bot.command(name="help") @bot.command(name="help")
@command_allowed_twitch
async def cmd_help(ctx): async def cmd_help(ctx):
parts = ctx.message.content.strip().split() if not await is_channel_live(bot):
cmd_name = parts[1] if len(parts) > 1 else None parts = ctx.message.content.strip().split()
await handle_help_command(ctx, cmd_name, bot, is_discord=False) cmd_name = parts[1] if len(parts) > 1 else None
await handle_help_command(ctx, cmd_name, bot, is_discord=False)
###################### ######################
# The following log entry must be last in the file to verify commands loading as they should # The following log entry must be last in the file to verify commands loading as they should

View File

@ -529,5 +529,8 @@
"Some artisanal cheeses are aged in natural caves, where unique microclimates contribute to complex flavors that are difficult to replicate in modern facilities.", "Some artisanal cheeses are aged in natural caves, where unique microclimates contribute to complex flavors that are difficult to replicate in modern facilities.",
"While Cheddar cheese is now a global staple, it originally came from the small English village of Cheddar, and its production methods have evolved dramatically over time.", "While Cheddar cheese is now a global staple, it originally came from the small English village of Cheddar, and its production methods have evolved dramatically over time.",
"Modern cheese production often uses vegetarian rennet, derived from microbial or plant sources, challenging the common belief that all cheese is made with animal-derived enzymes.", "Modern cheese production often uses vegetarian rennet, derived from microbial or plant sources, challenging the common belief that all cheese is made with animal-derived enzymes.",
"Cows are related to whales, as they both evolved from land-dwelling, even-toed ungulates, hence why a baby whale is called a 'calf'." "Cows are related to whales, as they both evolved from land-dwelling, even-toed ungulates, hence why a baby whale is called a 'calf'.",
"McNamee was the first to reach 999 of an item, sticks, and was awarded the sticker 'McNamee's Stick' for his efforts.",
"Jenni is 159cm (5.2 feet) tall, while Kami is 186cm (6.1 feet), making Kami almost 20% taller, or the length of a sheet of A4 paper.",
"If Jenni were to bury Kami's dead body in the back of the garden, she wouldn't be able to get out of the hole without a ladder."
] ]

View File

@ -102,7 +102,7 @@ def log(message: str, level="INFO", exec_info=False, linebreaks=False):
log_message = f"[{timestamp} - {uptime_str}] [{level}] {message}" log_message = f"[{timestamp} - {uptime_str}] [{level}] {message}"
# Include traceback for certain error levels # Include traceback for certain error levels
if exec_info or level in ["CRITICAL", "FATAL"]: if exec_info or level in ["ERROR", "CRITICAL", "FATAL"]:
log_message += f"\n{traceback.format_exc()}" log_message += f"\n{traceback.format_exc()}"
# Print to terminal if enabled # Print to terminal if enabled
@ -229,4 +229,9 @@ class Constants:
return_dict = {"object": primary_guild_object, "id": primary_guild_int} return_dict = {"object": primary_guild_object, "id": primary_guild_int}
return return_dict return return_dict
def twitch_channels_config(self):
with open("settings/twitch_channels_config.json", "r") as f:
CHANNEL_CONFIG = json.load(f)
return CHANNEL_CONFIG
constants = Constants() constants = Constants()

View File

@ -10,6 +10,7 @@ from typing import Union
from modules.db import run_db_operation, lookup_user, log_message from modules.db import run_db_operation, lookup_user, log_message
import modules.utility as utility import modules.utility as utility
import discord import discord
from functools import wraps
import globals import globals
@ -992,6 +993,54 @@ async def get_guild_info(bot: discord.Client, guild_id: Union[int, str]) -> dict
} }
return info return info
async def is_channel_live(bot = None) -> bool:
streams = await bot.fetch_streams(user_logins=["ookamikuntv"]) if bot else []
return bool(streams)
def list_channels(self):
# Command to list connected channels.
connected_channels = ", ".join(channel.name for channel in self.connected_channels)
globals.log(f"Currently connected to {connected_channels}")
def command_allowed_twitch(func):
"""
A custom check that allows a command to run based on channel settings.
It looks up the current channel in CHANNEL_CONFIG and either allows or denies
the command based on the filter mode and list.
"""
@wraps(func)
async def wrapper(ctx, *args, **kwargs):
# Load the full configuration.
full_config = globals.constants.twitch_channels_config()
# Get the channel name and then the channel-specific configuration.
channel_name = ctx.channel.name.lower()
channel_config = full_config.get(channel_name)
# If there's no configuration for this channel, block the command.
if not channel_config:
globals.log(f"No configuration found for Twitch channel '{channel_name}'. Blocking command '{ctx.command.name}'.")
return
mode = channel_config.get("commands_filter_mode")
filtered = channel_config.get("commands_filtered", [])
command_name = ctx.command.name
# Check based on filter mode.
if mode == "exclude":
if command_name in filtered:
globals.log(f"Command '{command_name}' is excluded on Twitch channel '{channel_name}'.")
return
elif mode == "include":
if command_name not in filtered:
globals.log(f"Command '{command_name}' is not allowed on Twitch channel '{channel_name}' (include mode).")
return
# If all checks pass, run the command.
return await func(ctx, *args, **kwargs)
return wrapper
############################################### ###############################################

View File

View File

@ -0,0 +1,10 @@
{
"ookamikuntv": {
"commands_filter_mode": "exclude",
"commands_filtered": []
},
"ookamipup": {
"commands_filter_mode": "exclude",
"commands_filtered": []
}
}

View File