Fixed Twitch bug

- Fixed an issue where Twitch users were not registered correctly due to recent DB changes
- Log-level of unidentified users changed from warning to info
- Minor changes to `!customvc`
  - Renamed `bitrate` -> `audio_bitrate`
  - Added `video_bitrate`
  - Changes some help text
  - Fixed certain subcommands which were broken after recent changes
  - Added `autoname` -> Automatically names the voice chat according to game being played by the owner
    - *Needs addiitonal work to implement all features*
kami_dev
Kami 2025-03-07 02:16:47 +01:00
parent cce0f21ab0
commit d1faf7f214
3 changed files with 196 additions and 158 deletions

View File

@ -1,4 +1,17 @@
# cmd_discord/customvc.py # cmd_discord/customvc.py
#
# TODO
# - Fix "allow" and "deny" subcommands not working (modifications not applied)
# - Fix "lock" subcommand not working (modifications not applied)
# - Add "video_bitrate" to subcommands list
# - Rename "bitrate" to "audio_bitrate"
# - Add automatic channel naming
# - Dynamic mode: displays the game currently (at any time) played by the owner, respecting conservative ratelimits
# - Static mode: names the channel according to pre-defined rules (used on creation if owner is not playing)
# - Add "autoname" to subcommands list
# - Sets the channel name to Dynamic Mode (see above)
# - Modify "settings" to display new information
import discord import discord
from discord.ext import commands from discord.ext import commands
@ -47,6 +60,8 @@ class CustomVCCog(commands.Cog):
"Subcommands:\n" "Subcommands:\n"
"- **name** <new_name> - rename\n" "- **name** <new_name> - rename\n"
" - `!customvc name my_custom_vc`\n" " - `!customvc name my_custom_vc`\n"
"- **autoname** - enables automatic renaming based on game presence\n"
" - `!customvc autoname`\n"
"- **claim** - claim ownership\n" "- **claim** - claim ownership\n"
" - `!customvc claim`\n" " - `!customvc claim`\n"
"- **lock** - lock the channel\n" "- **lock** - lock the channel\n"
@ -59,17 +74,23 @@ class CustomVCCog(commands.Cog):
" - `!customvc unlock`\n" " - `!customvc unlock`\n"
"- **users** <count> - set user limit\n" "- **users** <count> - set user limit\n"
" - `!customvc users 2`\n" " - `!customvc users 2`\n"
"- **bitrate** <kbps> - set channel bitrate\n" "- **audio_bitrate** <kbps> - set audio bitrate\n"
" - `!customvc bitrate some_bitrate`\n" " - `!customvc audio_bitrate 64`\n"
"- **status** <status_str> - set a custom status **(TODO)**\n" "- **video_bitrate** <kbps> - set video bitrate\n"
" - `!customvc video_bitrate 2500`\n"
"- ~~**status** <status_str> - set a custom status~~\n"
"- **op** <user> - co-owner\n" "- **op** <user> - co-owner\n"
" - `!customvc op some_user`\n" " - `!customvc op some_user`\n"
"- **settings** - show config\n" "- **settings** - show channel settings\n"
" - `!customvc settings`\n" " - `!customvc settings`\n"
) )
await ctx.reply(msg) await ctx.reply(msg)
else: else:
try:
await self.bot.invoke(ctx) # This will ensure subcommands get processed. await self.bot.invoke(ctx) # This will ensure subcommands get processed.
globals.log(f"{ctx.author.name} executed Custom VC subcommand '{ctx.invoked_subcommand}' in {ctx.channel.name}", "DEBUG")
except Exception as e:
globals.log(f"'customvc {ctx.invoked_subcommand}' failed to execute: {e}", "ERROR")
# Subcommands: # Subcommands:
@ -109,10 +130,14 @@ class CustomVCCog(commands.Cog):
await self.show_settings_logic(ctx) await self.show_settings_logic(ctx)
@customvc.command(name="users") @customvc.command(name="users")
async def set_users_limit(self, ctx): async def set_users_limit(self, ctx, *, limit: int):
"""Assign a VC users limit""" """Assign a VC users limit"""
await self.set_user_limit_logic(ctx) await self.set_user_limit_logic(ctx, limit)
@customvc.command(name="op")
async def op_user(self, ctx, *, user: str):
"""Make another user co-owner"""
await self.op_user_logic(ctx, user)
# #
# Main voice update logic # Main voice update logic
@ -166,10 +191,12 @@ class CustomVCCog(commands.Cog):
# Announce in the new VC's ephemeral chat # Announce in the new VC's ephemeral chat
if hasattr(new_vc, "send"): if hasattr(new_vc, "send"):
await new_vc.send( await new_vc.send(
f"{member.mention}, your custom voice channel is ready! " f"{member.name}, your custom voice channel is ready! "
"Type `!customvc` here for help with subcommands." "Type `!customvc` here for help with subcommands."
) )
await self.update_channel_name(member, after.channel)
# 3) If user left a custom VC -> maybe its now empty # 3) If user left a custom VC -> maybe its now empty
if before.channel and before.channel.id in self.CUSTOM_VC_INFO: if before.channel and before.channel.id in self.CUSTOM_VC_INFO:
old_vc = before.channel old_vc = before.channel
@ -304,9 +331,25 @@ class CustomVCCog(commands.Cog):
return await ctx.send("You are not in a custom voice channel.") return await ctx.send("You are not in a custom voice channel.")
if not self.is_initiator_or_mod(ctx, vc.id): if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to rename.") return await ctx.send("No permission to rename.")
await vc.edit(name=new_name) await vc.edit(name=new_name, reason=f'{ctx.author.name} renamed the channel.')
await ctx.send(f"Renamed channel to **{new_name}**.") await ctx.send(f"Renamed channel to **{new_name}**.")
@customvc.command(name="autoname")
async def enable_autoname(self, ctx: commands.Context):
"""Enables automatic channel naming based on the owner's game."""
vc = self.get_custom_vc_for(ctx)
if not vc:
return await ctx.send("Not in a custom VC.")
if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to enable auto-naming.")
info = self.CUSTOM_VC_INFO[vc.id]
info["autoname"] = True
await self.update_channel_name(ctx.author, vc)
await ctx.send("Auto-naming enabled! Your channel will update based on the game you're playing.")
async def claim_channel_logic(self, ctx: commands.Context): async def claim_channel_logic(self, ctx: commands.Context):
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
if not vc: if not vc:
@ -316,9 +359,14 @@ class CustomVCCog(commands.Cog):
return await ctx.send("No memory for this channel?") return await ctx.send("No memory for this channel?")
old = info["owner_id"] old = info["owner_id"]
# if author is owner
if old == ctx.author:
return await ctx.send("You're already the owner of this Custom VC!")
# if old owner is still inside # if old owner is still inside
if any(m.id == old for m in vc.members): if any(m.id == old for m in vc.members):
return await ctx.send("The original owner is still here; you cannot claim.") return await ctx.send(f"The current owner, {old.name}, is still here!.")
info["owner_id"] = ctx.author.id info["owner_id"] = ctx.author.id
await ctx.send("You are now the owner of this channel!") await ctx.send("You are now the owner of this channel!")
@ -331,10 +379,11 @@ class CustomVCCog(commands.Cog):
info = self.CUSTOM_VC_INFO[vc.id] info = self.CUSTOM_VC_INFO[vc.id]
info["locked"] = True info["locked"] = True
overwrites = vc.overwrites or {}
overwrites[ctx.guild.default_role] = discord.PermissionOverwrite(connect=False) # Use set_permissions() instead of modifying overwrites directly
await vc.edit(overwrites=overwrites) await vc.set_permissions(ctx.guild.default_role, connect=False, reason=f"{ctx.author.name} locked the Custom VC")
await ctx.send("Channel locked.")
await ctx.send("Channel locked. Only allowed users can join.")
async def allow_user_logic(self, ctx: commands.Context, user: str): async def allow_user_logic(self, ctx: commands.Context, user: str):
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
@ -346,11 +395,13 @@ class CustomVCCog(commands.Cog):
mem = await self.resolve_member(ctx, user) mem = await self.resolve_member(ctx, user)
if not mem: if not mem:
return await ctx.send(f"Could not find user: {user}.") return await ctx.send(f"Could not find user: {user}.")
info = self.CUSTOM_VC_INFO[vc.id] info = self.CUSTOM_VC_INFO[vc.id]
info["allowed_ids"].add(mem.id) info["allowed_ids"].add(mem.id)
overwrites = vc.overwrites or {}
overwrites[mem] = discord.PermissionOverwrite(connect=True) # Set explicit permissions instead of overwriting
await vc.edit(overwrites=overwrites) await vc.set_permissions(mem, connect=True, reason=f"{ctx.author.name} allowed {mem.display_name} into their Custom VC")
await ctx.send(f"Allowed **{mem.display_name}** to join.") await ctx.send(f"Allowed **{mem.display_name}** to join.")
async def deny_user_logic(self, ctx: commands.Context, user: str): async def deny_user_logic(self, ctx: commands.Context, user: str):
@ -363,16 +414,17 @@ class CustomVCCog(commands.Cog):
mem = await self.resolve_member(ctx, user) mem = await self.resolve_member(ctx, user)
if not mem: if not mem:
return await ctx.send(f"Could not find user: {user}.") return await ctx.send(f"Could not find user: {user}.")
info = self.CUSTOM_VC_INFO[vc.id] info = self.CUSTOM_VC_INFO[vc.id]
# check if they're mod or owner
if has_custom_vc_permission(mem, info["owner_id"]): if has_custom_vc_permission(mem, info["owner_id"]):
return await ctx.send("Cannot deny a moderator or the owner.") return await ctx.send("Cannot deny a moderator or the owner.")
info["denied_ids"].add(mem.id) info["denied_ids"].add(mem.id)
overwrites = vc.overwrites or {} # Explicitly deny permission
overwrites[mem] = discord.PermissionOverwrite(connect=False) await vc.set_permissions(mem, connect=False, reason=f"{ctx.author.name} denied {mem.display_name} from their Custom VC")
await vc.edit(overwrites=overwrites)
await ctx.send(f"Denied {mem.display_name} from connecting.") await ctx.send(f"Denied **{mem.display_name}** from connecting.")
async def unlock_channel_logic(self, ctx: commands.Context): async def unlock_channel_logic(self, ctx: commands.Context):
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
@ -381,47 +433,69 @@ class CustomVCCog(commands.Cog):
if not self.is_initiator_or_mod(ctx, vc.id): if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to unlock.") return await ctx.send("No permission to unlock.")
# Fetch category and its permissions
category = vc.category
if not category:
return await ctx.send("Error: Could not determine category permissions.")
# Copy category permissions
overwrites = category.overwrites.copy()
# Update stored channel info
info = self.CUSTOM_VC_INFO[vc.id] info = self.CUSTOM_VC_INFO[vc.id]
info["locked"] = False info["locked"] = False
overwrites = vc.overwrites or {}
overwrites[ctx.guild.default_role] = discord.PermissionOverwrite(connect=True)
for d_id in info["denied_ids"]: await vc.edit(overwrites=overwrites, reason=f'{ctx.author.name} unlocked their Custom VC')
m = ctx.guild.get_member(d_id) await ctx.send("Unlocked the channel. Permissions now match the category default.")
if m:
overwrites[m] = discord.PermissionOverwrite(connect=False)
await vc.edit(overwrites=overwrites)
await ctx.send("Unlocked the channel. Denied users still cannot join.")
async def set_user_limit_logic(self, ctx: commands.Context, limit: int): async def set_user_limit_logic(self, ctx: commands.Context, limit: int):
MIN = 2
MAX = 99
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
if not vc: if not vc:
return await ctx.send("Not in a custom VC.") return await ctx.send("Not in a custom VC.")
if limit < 2: if limit < MIN:
return await ctx.send("Minimum limit is 2.") return await ctx.send(f"Minimum limit is {MIN}.")
if limit > 99: if limit > MAX:
return await ctx.send("Maximum limit is 99.") return await ctx.send(f"Maximum limit is {MAX}.")
if not self.is_initiator_or_mod(ctx, vc.id): if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to set user limit.") return await ctx.send("No permission to set user limit.")
info = self.CUSTOM_VC_INFO[vc.id] info = self.CUSTOM_VC_INFO[vc.id]
info["user_limit"] = limit info["user_limit"] = limit
await vc.edit(user_limit=limit) await vc.edit(user_limit=limit, reason=f'{ctx.author.name} changed users limit to {limit} in their Custom VC')
await ctx.send(f"User limit set to {limit}.") await ctx.send(f"User limit set to {limit}.")
async def set_bitrate_logic(self, ctx: commands.Context, kbps: int): @customvc.command(name="audio_bitrate")
async def set_audio_bitrate(self, ctx: commands.Context, kbps: int):
"""Sets the audio bitrate for a VC."""
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
if not vc: if not vc:
return await ctx.send("Not in a custom VC.") return await ctx.send("Not in a custom VC.")
if not self.is_initiator_or_mod(ctx, vc.id): if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to set bitrate.") return await ctx.send("No permission to set audio bitrate.")
info = self.CUSTOM_VC_INFO[vc.id]
info["bitrate"] = kbps
if kbps < 8 or kbps > 128: if kbps < 8 or kbps > 128:
return await ctx.send(f"Invalid bitrate of {kbps} kbps! Must be a number between `8` and `128`! Default is 64 kbps") return await ctx.send(f"Invalid audio bitrate! Must be between 8 and 128 kbps.")
await vc.edit(bitrate=kbps * 1000)
await ctx.send(f"Bitrate set to {kbps} kbps.") await vc.edit(bitrate=kbps * 1000, reason=f"{ctx.author.name} changed audio bitrate")
await ctx.send(f"Audio bitrate set to {kbps} kbps.")
@customvc.command(name="video_bitrate")
async def set_video_bitrate(self, ctx: commands.Context, kbps: int):
"""Sets the video bitrate for a VC."""
vc = self.get_custom_vc_for(ctx)
if not vc:
return await ctx.send("Not in a custom VC.")
if not self.is_initiator_or_mod(ctx, vc.id):
return await ctx.send("No permission to set video bitrate.")
if kbps < 100 or kbps > 8000:
return await ctx.send(f"Invalid video bitrate! Must be between 100 and 8000 kbps.")
await vc.edit(video_quality_mode=discord.VideoQualityMode.full, bitrate=kbps * 1000, reason=f"{ctx.author.name} changed video bitrate")
await ctx.send(f"Video bitrate set to {kbps} kbps.")
async def set_status_logic(self, ctx: commands.Context, status_str: str): async def set_status_logic(self, ctx: commands.Context, status_str: str):
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
@ -434,6 +508,25 @@ class CustomVCCog(commands.Cog):
info["status"] = status_str info["status"] = status_str
await ctx.send(f"Channel status set to: **{status_str}** (placeholder).") await ctx.send(f"Channel status set to: **{status_str}** (placeholder).")
async def update_channel_name(self, member: discord.Member, vc: discord.VoiceChannel):
"""Dynamically renames VC based on the user's game or predefined rules."""
if not vc or not member:
return
game_name = None
if member.activities:
for activity in member.activities:
if isinstance(activity, discord.Game):
game_name = activity.name
break
if game_name:
new_name = f"{game_name}"
else:
new_name = f"{member.display_name}'s Channel"
await vc.edit(name=new_name, reason="Automatic VC Naming")
async def op_user_logic(self, ctx: commands.Context, user: str): async def op_user_logic(self, ctx: commands.Context, user: str):
vc = self.get_custom_vc_for(ctx) vc = self.get_custom_vc_for(ctx)
if not vc: if not vc:

View File

@ -481,7 +481,7 @@ def lookup_user(db_conn, identifier: str, identifier_type: str, target_identifie
# Handle no result case # Handle no result case
if not rows: if not rows:
globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "WARNING") globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "INFO")
return None return None
# Convert the row to a dictionary # Convert the row to a dictionary

View File

@ -666,6 +666,10 @@ async def send_message(ctx, text):
""" """
await ctx.send(text) await ctx.send(text)
import uuid
from modules.db import run_db_operation, lookup_user
import globals
def track_user_activity( def track_user_activity(
db_conn, db_conn,
platform: str, platform: str,
@ -675,145 +679,86 @@ def track_user_activity(
user_is_bot: bool = False user_is_bot: bool = False
): ):
""" """
Create or update a user record in the database for a given platform. Tracks or updates a user in the database.
This function checks whether a user with the specified user ID exists in the 'users' This function:
table for the provided platform (either "discord" or "twitch"). If a matching record is found, - Checks if the user already exists in Platform_Mapping.
the function compares the provided username, display name, and bot status with the stored values, - If found, updates username/display_name if changed.
updating the record if any discrepancies are detected. If no record exists, a new user record is - If not found, adds a new user and platform mapping entry.
created with a generated UUID.
Args: Args:
db_conn: The active database connection used to perform database operations. db_conn: Active database connection.
platform (str): The platform to which the user belongs. Expected values are "discord" or "twitch". platform (str): The platform of the user ("discord" or "twitch").
user_id (str): The unique identifier of the user on the given platform. user_id (str): The unique user identifier on the platform.
username (str): The raw username of the user (for Discord, this excludes the discriminator). username (str): The platform-specific username.
display_name (str): The display name of the user. display_name (str): The platform-specific display name.
user_is_bot (bool, optional): Indicates whether the user is a bot on the platform. user_is_bot (bool, optional): Indicates if the user is a bot. Defaults to False.
Defaults to False.
Returns: Returns:
None None
Side Effects:
- Logs debugging and error messages via the global logger.
- Updates an existing user record if discrepancies are found.
- Inserts a new user record if no existing record is found.
""" """
globals.log(f"UUI Lookup for: {username} - {user_id} ({platform.lower()}) ...", "DEBUG") platform = platform.lower()
valid_platforms = {"discord", "twitch"}
# Decide which column we use for the ID lookup ("discord_user_id" or "twitch_user_id") if platform not in valid_platforms:
if platform.lower() in ("discord", "twitch"):
identifier_type = f"{platform.lower()}_user_id"
else:
globals.log(f"Unknown platform '{platform}' in track_user_activity!", "WARNING") globals.log(f"Unknown platform '{platform}' in track_user_activity!", "WARNING")
return return
# 1) Try to find an existing user row. # Look up user by platform-specific ID in Platform_Mapping
user_data = lookup_user(db_conn, identifier=user_id, identifier_type=identifier_type) user_data = lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id")
if user_data: if user_data:
# Found an existing row for that user ID on this platform. # Existing user found, update info if necessary
# Check if the username or display_name is different and update if necessary. user_uuid = user_data["UUID"]
need_update = False need_update = False
column_updates = [] column_updates = []
params = [] params = []
globals.log(f"... Returned {user_data}", "DEBUG")
if platform.lower() == "discord":
if user_data["platform_username"] != username: if user_data["platform_username"] != username:
need_update = True need_update = True
column_updates.append("platform_username = ?") column_updates.append("Username = ?")
params.append(username) params.append(username)
if user_data["platform_display_name"] != display_name: if user_data["platform_display_name"] != display_name:
need_update = True need_update = True
column_updates.append("platform_display_name = ?") column_updates.append("Display_Name = ?")
params.append(display_name) params.append(display_name)
if user_data["user_is_bot"] != user_is_bot:
need_update = True
column_updates.append("user_is_bot = ?")
params.append(int(user_is_bot))
if need_update: if need_update:
set_clause = ", ".join(column_updates)
update_sql = f""" update_sql = f"""
UPDATE users UPDATE Platform_Mapping
SET {set_clause} SET {", ".join(column_updates)}
WHERE discord_user_id = ? WHERE Platform_User_ID = ? AND Platform_Type = ?
""" """
params.append(user_id) params.extend([user_id, platform.capitalize()])
rowcount = run_db_operation(db_conn, "update", update_sql, params)
rowcount = run_db_operation(db_conn, "update", update_sql, params=params)
if rowcount and rowcount > 0: if rowcount and rowcount > 0:
globals.log(f"Updated Discord user '{username}' (display '{display_name}') in 'users'.", "DEBUG") globals.log(f"Updated {platform.capitalize()} user '{username}' (display '{display_name}') in Platform_Mapping.", "DEBUG")
return
elif platform.lower() == "twitch": # If user was not found in Platform_Mapping, check Users table
if user_data["platform_username"] != username: user_uuid = str(uuid.uuid4())
need_update = True insert_user_sql = """
column_updates.append("platform_username = ?") INSERT INTO Users (UUID, Unified_Username, user_is_banned, user_is_bot)
params.append(username) VALUES (?, ?, 0, ?)
if user_data["platform_display_name"] != display_name:
need_update = True
column_updates.append("platform_display_name = ?")
params.append(display_name)
if user_data["user_is_bot"] != user_is_bot:
need_update = True
column_updates.append("user_is_bot = ?")
params.append(int(user_is_bot))
if need_update:
set_clause = ", ".join(column_updates)
update_sql = f"""
UPDATE users
SET {set_clause}
WHERE twitch_user_id = ?
""" """
params.append(user_id) run_db_operation(db_conn, "write", insert_user_sql, (user_uuid, username, int(user_is_bot)))
rowcount = run_db_operation(db_conn, "update", update_sql, params=params) # Insert into Platform_Mapping
if rowcount and rowcount > 0: insert_mapping_sql = """
globals.log(f"Updated Twitch user '{username}' (display '{display_name}') in 'users'.", "DEBUG") INSERT INTO Platform_Mapping (Platform_User_ID, Platform_Type, UUID, Display_Name, Username)
else:
# 2) No row found => create a new user row.
new_uuid = str(uuid.uuid4())
if platform.lower() == "discord":
insert_sql = """
INSERT INTO users (
UUID,
discord_user_id,
platform_username,
platform_display_name,
user_is_bot
)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""" """
params = (new_uuid, user_id, username, display_name, int(user_is_bot)) params = (user_id, platform.capitalize(), user_uuid, display_name, username)
else: # platform is "twitch" rowcount = run_db_operation(db_conn, "write", insert_mapping_sql, params)
insert_sql = """
INSERT INTO users (
UUID,
twitch_user_id,
platform_username,
platform_display_name,
user_is_bot
)
VALUES (?, ?, ?, ?, ?)
"""
params = (new_uuid, user_id, username, display_name, int(user_is_bot))
rowcount = run_db_operation(db_conn, "write", insert_sql, params)
if rowcount and rowcount > 0: if rowcount and rowcount > 0:
globals.log(f"Created new user row for {platform} user '{username}' (display '{display_name}') with UUID={new_uuid}.", "DEBUG") globals.log(f"Created new user entry for {platform} user '{username}' (display '{display_name}') with UUID={user_uuid}.", "DEBUG")
else: else:
globals.log(f"Failed to create new user row for {platform} user '{username}'", "ERROR") globals.log(f"Failed to create user entry for {platform} user '{username}'.", "ERROR")
from modules.db import log_bot_event from modules.db import log_bot_event