diff --git a/bot.py b/bot.py index 1a1385b..5e002ca 100644 --- a/bot.py +++ b/bot.py @@ -39,10 +39,14 @@ @bot.event async def on_ready() -> None: print(f'Bot started. \nUsername: {bot.user.name}. \nID: {bot.user.id}', flush=True) + guild = bot.get_guild(GUILD_ID) try: await bot.change_presence(activity=config.activity(), status=config.status()) - await bot.tree.sync() + if guild: + await bot.tree.sync(guild=guild) # Sync commands to a specific guild for faster deployment + else: + await bot.tree.sync() # Sync global commands (might take up to 1 hour to reflect globally) except BadArgument as e: print(f'Error changing presence. Exception - {e}', flush=True) diff --git a/cogs/staffings.py b/cogs/staffings.py index 9ae56c3..535984a 100644 --- a/cogs/staffings.py +++ b/cogs/staffings.py @@ -1,13 +1,14 @@ -from typing_extensions import Literal import discord import asyncio import re -from discord import app_commands, TextChannel +from discord import app_commands, TextChannel, Interaction from discord.ext import commands, tasks from datetime import datetime +from typing_extensions import Literal, List + from helpers.booking import Booking from helpers.message import staff_roles, is_obs from helpers.staffing_async import StaffingAsync @@ -34,10 +35,16 @@ def cog_unload(self): # SLASH COMMAND FUNCTIONS # ---------------------------------- # + + # A function to dynamically fetch and return a list of titles as choices + async def get_title_choices(self) -> List[app_commands.Choice[str]]: + titles = StaffingAsync._get_titles() + return [app_commands.Choice(name=title, value=title) for title in titles] + @app_commands.command(name="setupstaffing", description="Bot setups staffing information") @app_commands.describe(title="What should the title of the staffing be?", week_int="What should the week interval be? eg. 1 then the date will be selected each week.", section_amount="What should the section amount be? eg. 3 then there will be 3 sections.", restrict_booking="Should the staffing restrict booking to first section before allowing other sections too?") @app_commands.checks.has_any_role(*staff_roles()) - async def setup_staffing(self, interaction: discord.Integration, title: StaffingAsync._get_titles(), week_int: app_commands.Range[int, 1, 4], section_amount: app_commands.Range[int, 1, 4], restrict_booking: Literal["Yes", "No"], channel: TextChannel): + async def setup_staffing(self, interaction: Interaction, title: str, week_int: app_commands.Range[int, 1, 4], section_amount: app_commands.Range[int, 1, 4], restrict_booking: Literal["Yes", "No"], channel: TextChannel): ctx: commands.Context = await self.bot.get_context(interaction) interaction._baton = ctx dates = await StaffingAsync._geteventdate(self, title) @@ -93,20 +100,37 @@ async def setup_staffing(self, interaction: discord.Integration, title: Staffing DB.insert(self=self, table="positions", columns=['position', 'user', 'type', 'local_booking', 'start_time', 'end_time', 'event'], values=[pos, "", j, local[section_positions[x][pos]['local_booking']], section_positions[x][pos]['start_time'], section_positions[x][pos]['end_time'], event]) j += 1 + # Link this command to custom autocomplete for titles + @setup_staffing.autocomplete("title") + async def title_autocomplete(self, interaction: Interaction, current: str): + titles = await self.get_title_choices() + return [choice for choice in titles if current.lower() in choice.name.lower()][:25] # Limit to 25 choices (Discord limit) + + # A function to dynamically fetch and return a list of titles as choices + async def get_avail_title_choices(self) -> List[app_commands.Choice[str]]: + titles = StaffingAsync._get_avail_titles() + return [app_commands.Choice(name=title, value=title) for title in titles] + @app_commands.command(name="refreshevent", description="Bot refreshes selected event") @app_commands.describe(title="Which staffing would you like to refresh?") @app_commands.checks.has_any_role(*staff_roles()) - async def refreshevent(self, interaction: discord.Integration, title: StaffingAsync._get_avail_titles()): + async def refreshevent(self, interaction: discord.Integration, title: str): id = DB.select(table="staffing", columns=['id'], where=['title'], value={'title': title})[0] await StaffingAsync._updatemessage(self, id) ctx: commands.Context = await self.bot.get_context(interaction) interaction._baton = ctx await ctx.send(f"{ctx.author.mention} Event `{title}` has been refreshed", delete_after=5, ephemeral=True) + # Link this command to custom autocomplete for titles + @refreshevent.autocomplete("title") + async def title_autocomplete(self, interaction: Interaction, current: str): + titles = await self.get_avail_title_choices() + return [choice for choice in titles if current.lower() in choice.name.lower()][:25] # Limit to 25 choices (Discord limit) + @app_commands.command(name="manreset", description="Bot manually resets selected event") @app_commands.describe(title="Which staffing would you like to manually reset?") @app_commands.checks.has_any_role(*staff_roles()) - async def manreset(self, interaction: discord.Integration, title: StaffingAsync._get_avail_titles()): + async def manreset(self, interaction: discord.Integration, title: str): await self.bot.wait_until_ready() ctx: commands.Context = await self.bot.get_context(interaction) interaction._baton = ctx @@ -133,10 +157,16 @@ async def manreset(self, interaction: discord.Integration, title: StaffingAsync. except Exception as e: await ctx.send(f"{ctx.author.mention} The bot failed to manual reset `{title}` with error `{e}` at `{str(datetime.now().isoformat())}`", ephemeral=True) + # Link this command to custom autocomplete for titles + @manreset.autocomplete("title") + async def title_autocomplete(self, interaction: Interaction, current: str): + titles = await self.get_avail_title_choices() + return [choice for choice in titles if current.lower() in choice.name.lower()][:25] # Limit to 25 choices (Discord limit) + @app_commands.command(name="updatestaffing", description="Bot updates selected staffing") @app_commands.describe(title="Which staffing would you like to update?") @app_commands.checks.has_any_role(*staff_roles()) - async def updatestaffing(self, interaction: discord.Integration, title: StaffingAsync._get_avail_titles()): + async def updatestaffing(self, interaction: discord.Integration, title: str): ctx: commands.Context = await self.bot.get_context(interaction) interaction._baton = ctx try: @@ -145,6 +175,12 @@ async def updatestaffing(self, interaction: discord.Integration, title: Staffing except Exception as e: await ctx.send(f'Error updating staffing {title} - {e}') raise e + + # Link this command to custom autocomplete for titles + @updatestaffing.autocomplete("title") + async def title_autocomplete(self, interaction: Interaction, current: str): + titles = await self.get_avail_title_choices() + return [choice for choice in titles if current.lower() in choice.name.lower()][:25] # Limit to 25 choices (Discord limit) @app_commands.command(name="book", description="Bot books selected position for selected staffing") @app_commands.describe(position="Which position would you like to book?") diff --git a/cogs/tasks.py b/cogs/tasks.py index 7d37073..4351737 100644 --- a/cogs/tasks.py +++ b/cogs/tasks.py @@ -100,12 +100,20 @@ async def user_check(self, interaction: discord.Integration): async def sync_commands(self, override=False): now = datetime.now().isoformat() + guild = self.bot.get_guild(GUILD_ID) + try: if DEBUG == True and override == False: print("sync_commands skipped due to DEBUG ON. You can start manually with command instead.", flush=True) return + print("sync_commands started at " + str(datetime.now().isoformat()), flush=True) - await self.bot.tree.sync(guild=discord.Object(id=GUILD_ID)) + + if guild: + await self.bot.tree.sync(guild=guild) # Sync commands to a specific guild for faster deployment + else: + await self.bot.tree.sync() # Sync global commands (might take up to 1 hour to reflect globally) + print("sync_commands finished at " + str(datetime.now().isoformat()), flush=True) except Exception as e: print(f'Failed to sync commands with error - {e} - at - {now}', flush=True) diff --git a/helpers/staffing_async.py b/helpers/staffing_async.py index 39a1790..8e08b14 100644 --- a/helpers/staffing_async.py +++ b/helpers/staffing_async.py @@ -1,6 +1,6 @@ import re import requests -from typing import Literal +from typing import Literal, List from datetime import datetime, timedelta @@ -21,28 +21,34 @@ def __init__(self) -> None: # ASYNC DATA FUNCTIONS # ---------------------------------- # - def _get_titles() -> Literal: - events = DB.select(table='events', columns=[ - 'name'], amount='all') - staffings = DB.select( - table="staffing", columns=['title'], amount='all') - formatted_staffings = [] + def _get_titles() -> List[str]: + """ + Function to fetch and return a list of unique event titles from the database + excluding titles already used in staffing. + """ + # Fetch event names and staffing titles from the database + events = DB.select(table='events', columns=['name'], amount='all') + staffings = DB.select(table="staffing", columns=['title'], amount='all') + + formatted_staffings = set(" ".join(map(str, staffing)) for staffing in staffings) formatted_events = [] + # If no events are found, append a message indicating no availability if not events: formatted_events.append('None is available. Please try again later.') else: - for staffing in staffings: - formatted_staffings.append(" ".join(map(str, staffing))) - for event in events: formatted_event = " ".join(map(str, event)) + # Add the event only if it is not already present in staffing if formatted_event not in formatted_staffings: formatted_events.append(formatted_event) - return Literal[tuple(formatted_events)] + + # Return a list of unique event titles + return list(set(formatted_events)) - def _get_avail_titles() -> Literal: + def _get_avail_titles() -> List[str]: staffings = DB.select(table="staffing", columns=['title'], amount='all') + formatted_staffings = [] if not staffings: formatted_staffings.append('None is available. Please try again later.') @@ -50,7 +56,7 @@ def _get_avail_titles() -> Literal: for staffing in staffings: formatted_staffings.append(" ".join(map(str, staffing))) - return Literal[tuple(formatted_staffings)] + return list[set(formatted_staffings)] async def _get_description(self, ctx): """