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):
"""
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
while True:
if retries > 3:

View File

@ -7,6 +7,7 @@ import time
import traceback
import globals
from functools import partial
import twitchio.ext
from discord.ext import commands
from dotenv import load_dotenv

View File

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

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.",
"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.",
"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}"
# 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()}"
# Print to terminal if enabled
@ -229,4 +229,9 @@ class Constants:
return_dict = {"object": primary_guild_object, "id": primary_guild_int}
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()

View File

@ -10,6 +10,7 @@ from typing import Union
from modules.db import run_db_operation, lookup_user, log_message
import modules.utility as utility
import discord
from functools import wraps
import globals
@ -992,6 +993,54 @@ async def get_guild_info(bot: discord.Client, guild_id: Union[int, str]) -> dict
}
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