async def cmd_delgrp(ctx): """ Usage``: delgroup <name> Description: Deletes the given group from the collection of timer groups in the current guild. If `name` is not given or matches multiple groups, will prompt for group selection. Parameters:: name: The name of the group to delete. Related: group, groups, newgroup Examples``: delgroup Espresso """ try: timer = await ctx.get_timers_matching(ctx.arg_str, channel_only=False) except ResponseTimedOut: raise ResponseTimedOut( "Group selection timed out. No groups were deleted.") from None except UserCancelled: raise UserCancelled( "User cancelled group selection. No groups were deleted." ) from None if timer is None: return await ctx.error_reply("No matching timers found!") # Delete the timer ctx.client.interface.destroy_timer(timer) # Notify the user await ctx.reply("The group `{}` has been removed!".format(timer.name))
async def get_timers_matching(ctx, name_str, channel_only=True, info=False): """ Interactively get a guild timer matching the given string. Parameters ---------- name_str: str Name or partial name of a group timer in the current guild or channel. channel_only: bool Whether to match against the groups in the current channel or those in the whole guild. info: bool Whether to display some summary info about the timer in the selector. Returns: Timer Raises ------ cmdClient.lib.UserCancelled: Raised if the user manually cancels the selection. cmdClient.lib.ResponseTimedOut: Raised if the user fails to respond to the selector within `120` seconds. """ # Get the full timer list if channel_only: timers = ctx.client.interface.get_channel_timers(ctx.ch.id) else: timers = ctx.client.interface.get_guild_timers(ctx.guild.id) # If there are no timers, quit early if not timers: return None # Build a list of matching timers name_str = name_str.strip() timers = [ timer for timer in timers if name_str.lower() in timer.name.lower() ] if len(timers) == 0: return None elif len(timers) == 1: return timers[0] else: if info: select_from = [timer.oneline_summary() for timer in timers] else: select_from = [timer.name for timer in timers] try: selected = await ctx.selector( "Multiple matching groups found, please select one.", select_from) except ResponseTimedOut: raise ResponseTimedOut("Group selection timed out.") from None except UserCancelled: raise UserCancelled("User cancelled group selection.") from None return timers[selected]
async def listen_for(ctx, allowed_input=None, timeout=120, lower=True, check=None): """ Listen for a one of a particular set of input strings, sent in the current channel by `ctx.author`. When found, return the message containing them. Parameters ---------- allowed_input: Union(List(str), None) List of strings to listen for. Allowed to be `None` precisely when a `check` function is also supplied. timeout: int Number of seconds to wait before timing out. lower: bool Whether to shift the allowed and message strings to lowercase before checking. check: Function(message) -> bool Alternative custom check function. Returns: discord.Message The message that was matched. Raises ------ cmdClient.lib.ResponseTimedOut: Raised when no messages matching the given criteria are detected in `timeout` seconds. """ # Generate the check if it hasn't been provided if not check: # Quick check the arguments are sane if not allowed_input: raise ValueError("allowed_input and check cannot both be None") # Force a lower on the allowed inputs allowed_input = [s.lower() for s in allowed_input] # Create the check function def check(message): result = (message.author == ctx.author) result = result and (message.channel == ctx.ch) result = result and ((message.content.lower() if lower else message.content) in allowed_input) return result # Wait for a matching message, catch and transform the timeout try: message = await ctx.client.wait_for('message', check=check, timeout=timeout) except asyncio.TimeoutError: raise ResponseTimedOut( "Session timed out waiting for user response.") from None return message
async def newgroup_interactive(ctx, name=None, role=None, channel=None, clock_channel=None): """ Interactivly create a new study group. Takes keyword arguments to use any pre-existing data. """ try: if name is None: name = await ctx.input( "Please enter a friendly name for the new study group:") while role is None: role_str = await ctx.input( "Please enter the study group role. " "This role is given to people who join the group, " "and is used for notifications. " "It needs to be mentionable, and I need permission to give it to users.\n" "(Accepted input: Role name or partial name, role id, or role mention.)" ) role = await ctx.find_role(role_str.strip(), interactive=True) while channel is None: channel_str = await ctx.input( "Please enter the text channel to bind the group to. " "The group will only be accessible from commands in this channel, " "and the channel will host the pinned status message for this group.\n" "(Accepted input: Channel name or partial name, channel id or channel mention.)" ) channel = await ctx.find_channel(channel_str.strip(), interactive=True) while clock_channel is None: clock_channel_str = await ctx.input( "Please enter the group clock voice channel. " "The name of this channel will be updated with the current stage and time remaining. " "It is recommended that the channel only be visible to the study group role. " "I must have permission to update the name of this channel.\n" "(Accepted input: Channel name or partial name, channel id or channel mention.)" ) clock_channel = await ctx.find_channel( clock_channel_str.strip(), interactive=True, chan_type=discord.ChannelType.voice) except UserCancelled: raise UserCancelled("User cancelled during group creationa! " "No group was created.") from None except ResponseTimedOut: raise ResponseTimedOut( "Timed out waiting for a response during group creation! " "No group was created.") from None # We now have all the data we need return ctx.client.interface.create_timer(name, role, channel, clock_channel)
async def cmd_addgrp(ctx): """ Usage``: newgroup newgroup <name> newgroup <name>, <role>, <channel>, <clock channel> Description: Creates a new group with the specified properties. With no arguments or just `name` given, prompts for the remaining information. Parameters:: name: The name of the group to create. role: The role given to people who join the group. channel: The text channel which can access this group. clock channel: The voice channel displaying the status of the group timer. Related: group, groups, delgroup Examples``: newgroup Espresso newgroup Espresso, Study Group 1, #study-channel, #espresso-vc """ args = ctx.arg_str.split(",") args = [arg.strip() for arg in args] if len(args) == 4: name, role_str, channel_str, clockchannel_str = args # Find the specified objects try: role = await ctx.find_role(role_str.strip(), interactive=True) channel = await ctx.find_channel(channel_str.strip(), interactive=True) clockchannel = await ctx.find_channel(clockchannel_str.strip(), interactive=True) except UserCancelled: raise UserCancelled( "User cancelled selection, no group was created.") from None except ResponseTimedOut: raise ResponseTimedOut( "Selection timed out, no group was created.") from None # Create the timer timer = ctx.client.interface.create_timer(name, role, channel, clockchannel) elif len(args) >= 1 and args[0]: timer = await newgroup_interactive(ctx, name=args[0]) else: timer = await newgroup_interactive(ctx) await ctx.reply( "Group **{}** has been created and bound to channel {}.".format( timer.name, timer.channel.mention))
async def cmd_adminrole(ctx): """ Usage``: adminrole adminrole <role> Description: View the timer admin role (in the first usage), or set it to the provided role (in the second usage). The timer admin role allows creation and deletion of group timers, as well as modification of the guild registry and forcing timer operations. *Setting the timer admin role requires the guild permission `manage_guild`.* Parameters:: role: The name, partial name, or id of the new timer admin role. """ if ctx.arg_str: if not ctx.author.guild_permissions.manage_guild: return await ctx.error_reply( "You need the `manage_guild` permission to change the timer admin role." ) try: role = await ctx.find_role(ctx.arg_str, interactive=True) except UserCancelled: raise UserCancelled( "User cancelled role selection. Timer admin role unchanged." ) from None except ResponseTimedOut: raise ResponseTimedOut( "Role selection timed out. Timer admin role unchanged." ) from None ctx.client.config.guilds.set(ctx.guild.id, "timeradmin", role.id) await ctx.embedreply("Timer admin role set to {}.".format( role.mention), color=discord.Colour.green()) else: roleid = ctx.client.config.guilds.get(ctx.guild.id, "timeradmin") if roleid is None: return await ctx.embedreply( "No timer admin role set for this guild.") role = ctx.guild.get_role(roleid) if role is None: await ctx.embedreply( "Timer admin role set to a nonexistent role `{}`.".format( roleid)) else: await ctx.embedreply("Timer admin role is {}.".format(role.mention) )
async def input(ctx, msg="", timeout=120): """ Listen for a response in the current channel, from ctx.author. Returns the response from ctx.author, if it is provided. Parameters ---------- msg: string Allows a custom input message to be provided. Will use default message if not provided. timeout: int Number of seconds to wait before timing out. Raises ------ cmdClient.lib.ResponseTimedOut: Raised when ctx.author does not provide a response before the function times out. """ # Deliver prompt offer_msg = await ctx.reply(msg or "Please enter your input.") # Criteria for the input message def checks(m): return m.author == ctx.author and m.channel == ctx.ch # Listen for the reply try: result_msg = await ctx.client.wait_for("message", check=checks, timeout=timeout) except asnycio.TimeoutError: raise ResponseTimedOut( "Session timed out waiting for user response.") from None result = result_msg.content # Attempt to delete the prompt and reply messages try: await offer_msg.delete() await result_msg.delete() except Exception: pass return result
async def selector(ctx, header, select_from, timeout=120, max_len=20): """ Interactive routine to prompt the `ctx.author` to select an item from a list. Returns the list index that was selected. Parameters ---------- header: str String to put at the top of each page of selection options. Intended to be information about the list the user is selecting from. select_from: List(str) The list of strings to select from. timeout: int The number of seconds to wait before throwing `ResponseTimedOut`. max_len: int The maximum number of items to display on each page. Decrease this if the items are long, to avoid going over the char limit. Returns ------- int: The index of the list entry selected by the user. Raises ------ cmdClient.lib.UserCancelled: Raised if the user manually cancels the selection. cmdClient.lib.ResponseTimedOut: Raised if the user fails to respond to the selector within `timeout` seconds. """ # Handle improper arguments if len(select_from) == 0: raise ValueError( "Selection list passed to `selector` cannot be empty.") # Generate the selector pages footer = "Please type the number corresponding to your selection, or type `c` now to cancel." list_pages = paginate_list(select_from, block_length=max_len) pages = ["\n".join([header, page, footer]) for page in list_pages] # Post the pages in a paged message out_msg = await ctx.pager(pages) # Listen for valid input valid_input = [str(i + 1) for i in range(0, len(select_from))] + ['c', 'C'] try: result_msg = await ctx.listen_for(valid_input, timeout=timeout) except ResponseTimedOut: raise ResponseTimedOut("Selector timed out waiting for a response.") # Try and delete the selector message and the user response. try: await out_msg.delete() await result_msg.delete() except discord.NotFound: pass except discord.Forbidden: pass # Handle user cancellation if result_msg.content in ['c', 'C']: raise UserCancelled("User cancelled selection.") # The content must now be a valid index. Collect and return it. index = int(result_msg.content) - 1 return index
async def newgroup_interactive(ctx, name=None, role=None, channel=None, clock_channel=None): """ Interactively create a new study group. Takes keyword arguments to use any pre-existing data. """ try: if name is None: name = await ctx.input( "Please enter a friendly name for the new study group:") while role is None: role_str = await ctx.input( "Please enter the study group role.\n" "This role is given to people who join the group, " "and is used for notifications.\n" "I must have permission to mention this role and give it to members. " "Note that it must be below my highest role in the role list.\n" "(Accepted input: Role name or partial name, role id, role mention, or `c` to cancel.)" ) if role_str.lower() == 'c': raise UserCancelled role = await ctx.find_role(role_str.strip(), interactive=True) while channel is None: channel_str = await ctx.input( "Please enter the text channel to bind the group to.\n" "The group will only be accessible from commands in this channel, " "and the channel will host the pinned status message for this group.\n" "I must have the `MANAGE_MESSAGES` permission in this channel to pin the status message.\n" "(Accepted input: Channel name or partial name, channel id, channel mention, or `c` to cancel.)" ) if channel_str.lower() == 'c': raise UserCancelled channel = await ctx.find_channel( channel_str.strip(), interactive=True, chan_type=discord.ChannelType.text) while clock_channel is None: clock_channel_str = await ctx.input( "Please enter the group voice channel, or `s` to continue without an associated voice channel.\n" "The name of this channel will be updated with the current stage and time remaining, " "and members who join the channel will automatically be subscribed to the study group.\n" "I must have the `MANAGE_CHANNEL` permission in this channel to update the name.\n" "(Accepted input: Channel name or partial name, channel id, channel mention, " "or `s` to skip or `c` to cancel.)") if clock_channel_str.lower() == 's': break if clock_channel_str.lower() == 'c': raise UserCancelled clock_channel = await ctx.find_channel( clock_channel_str.strip(), interactive=True, chan_type=discord.ChannelType.voice) except UserCancelled: raise UserCancelled("User cancelled during group creation! " "No group was created.") from None except ResponseTimedOut: raise ResponseTimedOut( "Timed out waiting for a response during group creation! " "No group was created.") from None # We now have all the data we need return ctx.client.interface.create_timer(name, role, channel, clock_channel)
async def find_channel(ctx, userstr, interactive=False, collection=None, chan_type=None): """ Find a guild channel given a partial matching string, allowing custom channel collections and several behavioural switches. Parameters ---------- userstr: str String obtained from a user, expected to partially match a channel in the collection. The string will be tested against both the id and the name of the channel. interactive: bool Whether to offer the user a list of channels to choose from, or pick the first matching channel. collection: List(discord.Channel) Collection of channels to search amongst. If none, uses the full guild channel list. chan_type: discord.ChannelType Type of channel to restrict the collection to. Returns ------- discord.Channel: If a valid channel is found. None: If no valid channel has been found. Raises ------ cmdClient.lib.UserCancelled: If the user cancels interactive channel selection. cmdClient.lib.ResponseTimedOut: If the user fails to respond to interactive channel selection within `60` seconds` """ # Handle invalid situations and input if not ctx.guild: raise InvalidContext("Attempt to use find_channel outside of a guild.") if userstr == "": raise ValueError("User string passed to find_channel was empty.") # Create the collection to search from args or guild channels collection = collection if collection else ctx.guild.channels if chan_type is not None: collection = [chan for chan in collection if chan.type == chan_type] # If the user input was a number or possible channel mention, extract it chanid = userstr.strip('<#@&!>') chanid = int(chanid) if chanid.isdigit() else None searchstr = userstr.lower() # Find the channel chan = None # Check method to determine whether a channel matches def check(chan): return (chan.id == chanid) or (searchstr in chan.name.lower()) # Get list of matching roles channels = list(filter(check, collection)) if len(channels) == 0: # Nope chan = None elif len(channels) == 1: # Select our lucky winner chan = channels[0] else: # We have multiple matching channels! if interactive: # Interactive prompt with the list of channels chan_names = [chan.name for chan in channels] try: selected = await ctx.selector( "`{}` channels found matching `{}`!".format( len(channels), userstr), chan_names, timeout=60) except UserCancelled: raise UserCancelled( "User cancelled channel selection.") from None except ResponseTimedOut: raise ResponseTimedOut( "Channel selection timed out.") from None chan = channels[selected] else: # Just select the first one chan = channels[0] if chan is None: await ctx.error_reply( "Couldn't find a channel matching `{}`!".format(userstr)) return chan
async def find_member(ctx, userstr, interactive=False, collection=None): """ Find a guild member given a partial matching string, allowing custom member collections. Parameters ---------- userstr: str String obtained from a user, expected to partially match a member in the collection. The string will be tested against both the userid, full user name and user nickname. interactive: bool Whether to offer the user a list of members to choose from, or pick the first matching channel. collection: List(discord.Member) Collection of members to search amongst. If none, uses the full guild member list. Returns ------- discord.Member: If a valid member is found. None: If no valid member has been found. Raises ------ cmdClient.lib.UserCancelled: If the user cancels interactive member selection. cmdClient.lib.ResponseTimedOut: If the user fails to respond to interactive member selection within `60` seconds` """ # Handle invalid situations and input if not ctx.guild: raise InvalidContext("Attempt to use find_member outside of a guild.") if userstr == "": raise ValueError("User string passed to find_member was empty.") # Create the collection to search from args or guild members collection = collection if collection else ctx.guild.members # If the user input was a number or possible member mention, extract it userid = userstr.strip('<#@&!>') userid = int(userid) if userid.isdigit() else None searchstr = userstr.lower() # Find the member member = None # Check method to determine whether a member matches def check(member): return (member.id == userid or searchstr in member.display_name.lower() or searchstr in str(member).lower()) # Get list of matching roles members = list(filter(check, collection)) if len(members) == 0: # Nope member = None elif len(members) == 1: # Select our lucky winner member = members[0] else: # We have multiple matching members! if interactive: # Interactive prompt with the list of members member_names = [ "{} {}".format( member.nick if member.nick else (member if members.count(member) > 1 else member.name), ("<{}>".format(member)) if member.nick else "") for member in members ] try: selected = await ctx.selector( "`{}` members found matching `{}`!".format( len(members), userstr), member_names, timeout=60) except UserCancelled: raise UserCancelled( "User cancelled member selection.") from None except ResponseTimedOut: raise ResponseTimedOut("Member selection timed out.") from None member = members[selected] else: # Just select the first one member = members[0] if member is None: await ctx.error_reply( "Couldn't find a member matching `{}`!".format(userstr)) return member