Example #1
0
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))
Example #2
0
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]
Example #3
0
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
Example #4
0
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)
Example #5
0
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))
Example #6
0
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)
                                 )
Example #7
0
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
Example #8
0
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
Example #9
0
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)
Example #10
0
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
Example #11
0
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