From d1faf7f214b4aae81e2b3311b734b2dd58e3570f Mon Sep 17 00:00:00 2001 From: Kami Date: Fri, 7 Mar 2025 02:16:47 +0100 Subject: [PATCH] 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* --- cmd_discord/customvc.py | 179 ++++++++++++++++++++++++++++++---------- modules/db.py | 2 +- modules/utility.py | 173 +++++++++++++------------------------- 3 files changed, 196 insertions(+), 158 deletions(-) diff --git a/cmd_discord/customvc.py b/cmd_discord/customvc.py index faac9ed..1f60bbf 100644 --- a/cmd_discord/customvc.py +++ b/cmd_discord/customvc.py @@ -1,4 +1,17 @@ # 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 from discord.ext import commands @@ -47,6 +60,8 @@ class CustomVCCog(commands.Cog): "Subcommands:\n" "- **name** - rename\n" " - `!customvc name my_custom_vc`\n" + "- **autoname** - enables automatic renaming based on game presence\n" + " - `!customvc autoname`\n" "- **claim** - claim ownership\n" " - `!customvc claim`\n" "- **lock** - lock the channel\n" @@ -59,17 +74,23 @@ class CustomVCCog(commands.Cog): " - `!customvc unlock`\n" "- **users** - set user limit\n" " - `!customvc users 2`\n" - "- **bitrate** - set channel bitrate\n" - " - `!customvc bitrate some_bitrate`\n" - "- **status** - set a custom status **(TODO)**\n" + "- **audio_bitrate** - set audio bitrate\n" + " - `!customvc audio_bitrate 64`\n" + "- **video_bitrate** - set video bitrate\n" + " - `!customvc video_bitrate 2500`\n" + "- ~~**status** - set a custom status~~\n" "- **op** - co-owner\n" " - `!customvc op some_user`\n" - "- **settings** - show config\n" + "- **settings** - show channel settings\n" " - `!customvc settings`\n" ) await ctx.reply(msg) else: - await self.bot.invoke(ctx) # This will ensure subcommands get processed. + try: + 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: @@ -109,10 +130,14 @@ class CustomVCCog(commands.Cog): await self.show_settings_logic(ctx) @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""" - 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 @@ -166,10 +191,12 @@ class CustomVCCog(commands.Cog): # Announce in the new VC's ephemeral chat if hasattr(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." ) + await self.update_channel_name(member, after.channel) + # 3) If user left a custom VC -> maybe it’s now empty if before.channel and before.channel.id in self.CUSTOM_VC_INFO: old_vc = before.channel @@ -304,9 +331,25 @@ class CustomVCCog(commands.Cog): return await ctx.send("You are not in a custom voice channel.") if not self.is_initiator_or_mod(ctx, vc.id): 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}**.") + @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): vc = self.get_custom_vc_for(ctx) if not vc: @@ -316,9 +359,14 @@ class CustomVCCog(commands.Cog): return await ctx.send("No memory for this channel?") 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 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 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["locked"] = True - overwrites = vc.overwrites or {} - overwrites[ctx.guild.default_role] = discord.PermissionOverwrite(connect=False) - await vc.edit(overwrites=overwrites) - await ctx.send("Channel locked.") + + # Use set_permissions() instead of modifying overwrites directly + await vc.set_permissions(ctx.guild.default_role, connect=False, reason=f"{ctx.author.name} locked the Custom VC") + + await ctx.send("Channel locked. Only allowed users can join.") async def allow_user_logic(self, ctx: commands.Context, user: str): vc = self.get_custom_vc_for(ctx) @@ -346,11 +395,13 @@ class CustomVCCog(commands.Cog): mem = await self.resolve_member(ctx, user) if not mem: return await ctx.send(f"Could not find user: {user}.") + info = self.CUSTOM_VC_INFO[vc.id] info["allowed_ids"].add(mem.id) - overwrites = vc.overwrites or {} - overwrites[mem] = discord.PermissionOverwrite(connect=True) - await vc.edit(overwrites=overwrites) + + # Set explicit permissions instead of overwriting + 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.") 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) if not mem: return await ctx.send(f"Could not find user: {user}.") + info = self.CUSTOM_VC_INFO[vc.id] - # check if they're mod or owner if has_custom_vc_permission(mem, info["owner_id"]): return await ctx.send("Cannot deny a moderator or the owner.") + info["denied_ids"].add(mem.id) - overwrites = vc.overwrites or {} - overwrites[mem] = discord.PermissionOverwrite(connect=False) - await vc.edit(overwrites=overwrites) - await ctx.send(f"Denied {mem.display_name} from connecting.") + # Explicitly deny permission + await vc.set_permissions(mem, connect=False, reason=f"{ctx.author.name} denied {mem.display_name} from their Custom VC") + + await ctx.send(f"Denied **{mem.display_name}** from connecting.") async def unlock_channel_logic(self, ctx: commands.Context): 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): 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["locked"] = False - overwrites = vc.overwrites or {} - overwrites[ctx.guild.default_role] = discord.PermissionOverwrite(connect=True) - for d_id in info["denied_ids"]: - m = ctx.guild.get_member(d_id) - if m: - overwrites[m] = discord.PermissionOverwrite(connect=False) - - await vc.edit(overwrites=overwrites) - await ctx.send("Unlocked the channel. Denied users still cannot join.") + await vc.edit(overwrites=overwrites, reason=f'{ctx.author.name} unlocked their Custom VC') + await ctx.send("Unlocked the channel. Permissions now match the category default.") async def set_user_limit_logic(self, ctx: commands.Context, limit: int): + MIN = 2 + MAX = 99 vc = self.get_custom_vc_for(ctx) if not vc: return await ctx.send("Not in a custom VC.") - if limit < 2: - return await ctx.send("Minimum limit is 2.") - if limit > 99: - return await ctx.send("Maximum limit is 99.") + if limit < MIN: + return await ctx.send(f"Minimum limit is {MIN}.") + if limit > MAX: + return await ctx.send(f"Maximum limit is {MAX}.") if not self.is_initiator_or_mod(ctx, vc.id): return await ctx.send("No permission to set user limit.") info = self.CUSTOM_VC_INFO[vc.id] 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}.") - 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) 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 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: - return await ctx.send(f"Invalid bitrate of {kbps} kbps! Must be a number between `8` and `128`! Default is 64 kbps") - await vc.edit(bitrate=kbps * 1000) - await ctx.send(f"Bitrate set to {kbps} kbps.") + return await ctx.send(f"Invalid audio bitrate! Must be between 8 and 128 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): vc = self.get_custom_vc_for(ctx) @@ -434,6 +508,25 @@ class CustomVCCog(commands.Cog): info["status"] = status_str 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): vc = self.get_custom_vc_for(ctx) if not vc: diff --git a/modules/db.py b/modules/db.py index 65df016..3730baf 100644 --- a/modules/db.py +++ b/modules/db.py @@ -481,7 +481,7 @@ def lookup_user(db_conn, identifier: str, identifier_type: str, target_identifie # Handle no result case 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 # Convert the row to a dictionary diff --git a/modules/utility.py b/modules/utility.py index f3a8c3b..d462f60 100644 --- a/modules/utility.py +++ b/modules/utility.py @@ -666,6 +666,10 @@ async def send_message(ctx, text): """ await ctx.send(text) +import uuid +from modules.db import run_db_operation, lookup_user +import globals + def track_user_activity( db_conn, platform: str, @@ -675,145 +679,86 @@ def track_user_activity( 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' - table for the provided platform (either "discord" or "twitch"). If a matching record is found, - the function compares the provided username, display name, and bot status with the stored values, - updating the record if any discrepancies are detected. If no record exists, a new user record is - created with a generated UUID. + This function: + - Checks if the user already exists in Platform_Mapping. + - If found, updates username/display_name if changed. + - If not found, adds a new user and platform mapping entry. Args: - db_conn: The active database connection used to perform database operations. - platform (str): The platform to which the user belongs. Expected values are "discord" or "twitch". - user_id (str): The unique identifier of the user on the given platform. - username (str): The raw username of the user (for Discord, this excludes the discriminator). - display_name (str): The display name of the user. - user_is_bot (bool, optional): Indicates whether the user is a bot on the platform. - Defaults to False. + db_conn: Active database connection. + platform (str): The platform of the user ("discord" or "twitch"). + user_id (str): The unique user identifier on the platform. + username (str): The platform-specific username. + display_name (str): The platform-specific display name. + user_is_bot (bool, optional): Indicates if the user is a bot. Defaults to False. Returns: 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") - - # Decide which column we use for the ID lookup ("discord_user_id" or "twitch_user_id") - if platform.lower() in ("discord", "twitch"): - identifier_type = f"{platform.lower()}_user_id" - else: + platform = platform.lower() + valid_platforms = {"discord", "twitch"} + + if platform not in valid_platforms: globals.log(f"Unknown platform '{platform}' in track_user_activity!", "WARNING") return - # 1) Try to find an existing user row. - user_data = lookup_user(db_conn, identifier=user_id, identifier_type=identifier_type) + # Look up user by platform-specific ID in Platform_Mapping + user_data = lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id") if user_data: - # Found an existing row for that user ID on this platform. - # Check if the username or display_name is different and update if necessary. + # Existing user found, update info if necessary + user_uuid = user_data["UUID"] need_update = False column_updates = [] params = [] - globals.log(f"... Returned {user_data}", "DEBUG") + if user_data["platform_username"] != username: + need_update = True + column_updates.append("Username = ?") + params.append(username) - if platform.lower() == "discord": - if user_data["platform_username"] != username: - need_update = True - column_updates.append("platform_username = ?") - params.append(username) + if user_data["platform_display_name"] != display_name: + need_update = True + column_updates.append("Display_Name = ?") + params.append(display_name) - if user_data["platform_display_name"] != display_name: - need_update = True - column_updates.append("platform_display_name = ?") - params.append(display_name) + if need_update: + update_sql = f""" + UPDATE Platform_Mapping + SET {", ".join(column_updates)} + WHERE Platform_User_ID = ? AND Platform_Type = ? + """ + params.extend([user_id, platform.capitalize()]) + rowcount = run_db_operation(db_conn, "update", update_sql, params) - 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 rowcount and rowcount > 0: + globals.log(f"Updated {platform.capitalize()} user '{username}' (display '{display_name}') in Platform_Mapping.", "DEBUG") + return - if need_update: - set_clause = ", ".join(column_updates) - update_sql = f""" - UPDATE users - SET {set_clause} - WHERE discord_user_id = ? - """ - params.append(user_id) + # If user was not found in Platform_Mapping, check Users table + user_uuid = str(uuid.uuid4()) + insert_user_sql = """ + INSERT INTO Users (UUID, Unified_Username, user_is_banned, user_is_bot) + VALUES (?, ?, 0, ?) + """ + 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) - if rowcount and rowcount > 0: - globals.log(f"Updated Discord user '{username}' (display '{display_name}') in 'users'.", "DEBUG") - - elif platform.lower() == "twitch": - if user_data["platform_username"] != username: - need_update = True - column_updates.append("platform_username = ?") - params.append(username) - - 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) - - rowcount = run_db_operation(db_conn, "update", update_sql, params=params) - if rowcount and rowcount > 0: - globals.log(f"Updated Twitch user '{username}' (display '{display_name}') in 'users'.", "DEBUG") + # Insert into Platform_Mapping + insert_mapping_sql = """ + INSERT INTO Platform_Mapping (Platform_User_ID, Platform_Type, UUID, Display_Name, Username) + VALUES (?, ?, ?, ?, ?) + """ + params = (user_id, platform.capitalize(), user_uuid, display_name, username) + rowcount = run_db_operation(db_conn, "write", insert_mapping_sql, params) + if rowcount and rowcount > 0: + globals.log(f"Created new user entry for {platform} user '{username}' (display '{display_name}') with UUID={user_uuid}.", "DEBUG") else: - # 2) No row found => create a new user row. - new_uuid = str(uuid.uuid4()) + globals.log(f"Failed to create user entry for {platform} user '{username}'.", "ERROR") - if platform.lower() == "discord": - insert_sql = """ - INSERT INTO users ( - UUID, - discord_user_id, - platform_username, - platform_display_name, - user_is_bot - ) - VALUES (?, ?, ?, ?, ?) - """ - params = (new_uuid, user_id, username, display_name, int(user_is_bot)) - else: # platform is "twitch" - 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: - globals.log(f"Created new user row for {platform} user '{username}' (display '{display_name}') with UUID={new_uuid}.", "DEBUG") - else: - globals.log(f"Failed to create new user row for {platform} user '{username}'", "ERROR") from modules.db import log_bot_event