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 bugfixeskami_dev
parent
6da1744990
commit
17fbb20cdc
|
@ -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:
|
||||||
|
|
1
bots.py
1
bots.py
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."
|
||||||
]
|
]
|
|
@ -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()
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###############################################
|
###############################################
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"ookamikuntv": {
|
||||||
|
"commands_filter_mode": "exclude",
|
||||||
|
"commands_filtered": []
|
||||||
|
},
|
||||||
|
"ookamipup": {
|
||||||
|
"commands_filter_mode": "exclude",
|
||||||
|
"commands_filtered": []
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue