async def add_first_emojis(message):
    """
    Scans the message for menu entries (see `activate_side_games` command) and
    adds the appropriate emoji reactions to the message, allowing other members
    to click on the emoji icons.

    Note that messages made by non-admins and messages in channels for which
    the side games voice channel feature is not enabled are ignored.
    """
    if not checks.is_admin(message.author):
        return  # ignore non-admin message
    if checks.author_is_me(message):
        return  # ignore bot messages

    if checks.is_event_channel(message.channel):
        translations = get_emoji_event_channels_translations(message)
        for emoji in translations.keys():
            await message.add_reaction(emoji)
        return  # return if message in event channel

    if not checks.is_side_games_channel(message.channel):
        return  # ignore messages in non-games channels

    translations = get_emoji_side_game_translations(message)
    for emoji in translations.keys():
        await message.add_reaction(emoji)
Beispiel #2
0
class IceCream(commands.Cog):
    def __init__(self, client):
        self.client = client

    @commands.command()
    @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin())
    async def icecream(self,
                       ctx: commands.Context,
                       day: Optional[str] = None) -> None:
        """Gives the ice-cream flavors on the menu for the given day.

        Parameters
        -----------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        day: `str`
            The day to find icecream flavors for.
        """

        POSSIBLE_DAYS: List[str] = [d.upper() for d in calendar.day_name]
        EST = datetime.now(timezone("US/Eastern"))
        TODAY = EST.strftime("%A").upper()
        if day is None:
            day = TODAY
        day = day.strip().upper()
        if day in POSSIBLE_DAYS:
            pass
        elif day in "TOMORROW":
            day = POSSIBLE_DAYS[(POSSIBLE_DAYS.index(TODAY) + 1) %
                                len(POSSIBLE_DAYS)]
        else:
            await ctx.send("Error: Not a valid day.")
            return
        flavors = nu_dining.ICE_CREAM_FLAVORS[day]
        await ctx.send(f"There is {flavors} on {day}.")
async def close_party(rp: ReactionPayload) -> None:
    """
    Emoji handler that implements the party close feature.

    If the reacting member is not the party leader or a bot admin as specified
    in `config.BOT_ADMIN_ROLES`, the emoji is removed and no further action is
    taken.

    Otherwise, the party message the party affiliations (membership,
    leadership) are deleted and an appropriate message is posted to the party
    matchmaking channel.
    """

    party = await Party.from_party_message(rp.message)
    channel = party.channel
    if party.leader != rp.member and not checks.is_admin(rp.member):
        await rp.message.remove_reaction(Emojis.NO_ENTRY_SIGN, rp.member)
        return
    if rp.member != party.leader:
        message = await channel.send(f"> {rp.member.mention} has just force "
                                     f"closed {party.leader.mention}'s party!")
    else:
        message = await channel.send(f"> {rp.member.mention} has just "
                                     f"disbanded their party!\n")
    await rp.message.delete()
    for m in party.members:
        db.party_channels[channel.id].clear_party_message_of_user(m)
    db.party_channels[channel.id].clear_party_message_of_user(party.leader)
    scheduling.message_delayed_delete(message)
Beispiel #4
0
class Clear(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.MESSAGE_DELETE_CAPACITY = 500
        self.MEMBER_DELETE_CAPACITY = 100

    @commands.command()
    @commands.guild_only()
    @commands.check_any(is_admin(), is_mod())
    async def clear(self,
                    ctx: commands.Context,
                    amount: int = 1,
                    member: discord.Member = None) -> None:
        """Deletes a set amount of message from the current channel.

        Parameters
        ------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        amount: `int`
            The amount of message to delete from the current channel.
        member: `discord.Member`
            Will delete messages only from the given member.
        """

        channel = ctx.channel
        await ctx.message.delete()

        if amount <= 0:
            await ctx.send("Cannot delete less than 1 message.")
            return

        deleted_messages: List[discord.Message] = []
        if member is None:
            if amount > self.MESSAGE_DELETE_CAPACITY:
                await ctx.send(
                    f"Cannot delete more than {self.MESSAGE_DELETE_CAPACITY} "
                    "messages at a time.")
                return
            deleted_messages = await channel.purge(limit=amount)
        else:
            if amount > self.MEMBER_DELETE_CAPACITY:
                await ctx.send(
                    f"Cannot delete more than {self.MEMBER_DELETE_CAPACITY} "
                    "messages when a member is mentioned.")
                return
            num_deleted: int = 0
            async for message in channel.history(limit=None):
                if num_deleted == amount:
                    break
                if message.author == member:
                    deleted_messages.append(message)
                    num_deleted += 1
            await channel.delete_messages(deleted_messages)
        await ctx.send(f"{len(deleted_messages)} messages deleted.",
                       delete_after=5)
Beispiel #5
0
class Misc(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.start_time = datetime.utcnow()

    @commands.command()
    @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(),
                        is_mod())
    async def ping(self, ctx: commands.Context):
        """Sends a message which contains the Discord WebSocket protocol latency.

        Parameters
        -------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        """

        await ctx.send(f"Pong! {round(self.client.latency * 1000)}ms")

    @commands.command()
    @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(),
                        is_mod())
    async def uptime(self, ctx: commands.Context):
        """Calculates the amount of time the bot has been up since it last started.

        Parameters
        -------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        """
        delta_uptime = datetime.utcnow() - self.start_time
        hours, remainder = divmod(int(delta_uptime.total_seconds()), 3600)
        minutes, seconds = divmod(remainder, 60)
        days, hours = divmod(hours, 24)
        await ctx.send(f"Uptime: `{days}d, {hours}h, {minutes}m, {seconds}s`")

    @is_admin()
    @commands.command()
    async def echo(self, ctx: commands.Context) -> None:
        """Repeats the message given after the command.
        Will use the escaped content which does not mention anything.

        Parameters
        -------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        """
        message: discord.Message = ctx.message
        await message.delete()
        content: str = message.clean_content[6:]
        if content:
            await ctx.send(content)
        else:
            await ctx.send("You didn't give anything to repeat",
                           delete_after=5)

    @commands.command()
    @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(),
                        is_mod())
    async def flip(self, ctx: commands.Context) -> None:
        """Flips an imaginary coin and sends the result.

        Parameters
        -------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        """

        results = ["Heads!", "Tails!"]
        outcome = random.randint(0, 1)
        await ctx.send(results[outcome])

    @commands.command()
    @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(),
                        is_mod())
    async def menu(self, ctx: commands.Context) -> None:
        """Sends a link to the Northeastern dining hall menu.

        Parameters
        -------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        """
        await ctx.send("https://nudining.com/public/menus")
Beispiel #6
0
class HallOfFame(commands.Cog):
    """Handles the automation behind the hall-of-fame channel.
    If at least [self.reaction_threshold] reactions with the 🏆 emoji are added to a
    message, then an embedded message with the content of the message, a link to the
    channel, and a link to the message are posted in the hall-of-fame channel.

    Note:
        - If the user who reacts to the message is the same person who sent it,
    it will not count.
        - If a moderator reacts with the designated mod hall-of-fame emoji, then it
    will automatically be send in the hall-of-fame channel regardless of emoji count.

    Attributes
    ----------
    client : `commands.Bot`
        a client connection to Discord to interact with the Discord WebSocket and APIs
    reaction_threshold: `int`
        the number of emojis that need to be reacted with (excluding the user who sent
        the message) in order to enter the hall-of-fame.
    hof_emoji: `str`
        The emoji which is tracked in order to determine whether to send it to the
        hall-of-fame or not.
    mod_hof_emoji: `str`
        An override emoji. When reacted to a message with it will automatically be sent
        in hall-of-fame regardless of the emoji count on the current message.
    """
    def __init__(self, client: Bot):
        self.client = client
        self.reaction_threshold: int = 5
        self.hof_emoji: str = "🏆"
        self.mod_hof_emoji: str = "🏅"
        self.hof_blacklist: Dict[int, Set[int]] = defaultdict(set)
        for document in self.client.db.get_hof_blacklist():
            guild_id: int = document["guild_id"]
            channels: List[int] = document["channels"]
            self.hof_blacklist[guild_id] = set(channels)

    @commands.is_owner()
    @commands.command(aliases=["setHOFThreshold"])
    @required_configs(ChannelType.HOF)
    async def set_hof_threshold(self, ctx: commands.Context,
                                threshold: int) -> None:
        """Sets the new reaction threshold."""
        await ctx.send(
            f"Hall of Fame reaction threshold set to `{threshold}` from `{self.reaction_threshold}`"
        )
        self.reaction_threshold = threshold

    @commands.check_any(is_admin(), is_mod())
    @commands.command(aliases=["addHOFBlacklist"])
    @required_configs(ChannelType.HOF)
    async def add_hof_blacklist(self, ctx: commands.Context,
                                channel: discord.TextChannel):
        guild: discord.Guild = ctx.guild
        if channel.id in self.hof_blacklist[guild.id]:
            return await ctx.send(
                "This channel is already being blacklisted by HOF.")
        self.client.db.add_to_hof_blacklist(guild.id, channel.id)
        self.hof_blacklist[guild.id].add(channel.id)
        await ctx.send(f"{channel.mention} has been added to the HOF blacklist"
                       )

    @commands.command(aliases=["removeHOFBlacklist", "rmHOFBlacklist"])
    @required_configs(ChannelType.HOF)
    async def remove_hof_blacklist(self, ctx: commands.Context,
                                   channel: discord.TextChannel):
        guild: discord.Guild = ctx.guild
        if channel.id not in self.hof_blacklist[guild.id]:
            return await ctx.send(
                "This channel was never blacklisted in the first place")
        self.client.db.remove_from_hof_blacklist(ctx.guild.id, channel.id)
        self.hof_blacklist[ctx.guild.id].remove(channel.id)
        await ctx.send(
            f"{channel.mention} has been removed from the HOF blacklist")

    @commands.command(aliases=["listHOFBlacklist", "lsHOFBlacklist"])
    @required_configs(ChannelType.HOF)
    async def list_hof_blacklist(self, ctx: commands.Context):
        guild: discord.Guild = ctx.guild
        blacklisted_channel_mentions: List[str] = [
            guild.get_channel(channel_id).mention
            for channel_id in self.hof_blacklist[guild.id]
        ]
        await ctx.send(
            f"There are {len(blacklisted_channel_mentions)} channels being blacklisted for HOF currently: {','.join(blacklisted_channel_mentions)}"
        )

    @commands.Cog.listener()
    @required_configs(ChannelType.HOF)
    async def on_raw_reaction_add(
            self, payload: discord.RawReactionActionEvent) -> None:
        """Checks if enough reactions were added and if so sends a message
        in the hall-of-fame channel.

        Parameters
        -------------
        payload: `discord.RawReactionActionEvent`
            The reaction payload with information about the event.
        """
        guild_id: int = payload.guild_id
        channel_id: int = payload.channel_id
        if channel_id in self.hof_blacklist[guild_id]:
            return

        emoji: discord.PartialEmoji = payload.emoji
        mod_emoji_used: bool = emoji.name == self.mod_hof_emoji
        if emoji.name != self.hof_emoji and not mod_emoji_used:
            return

        message_id: int = payload.message_id
        if self.client.db.message_in_hof(guild_id, message_id):
            return

        guild: discord.Guild = self.client.get_guild(guild_id)
        channel: discord.TextChannel = guild.get_channel(channel_id)
        message: discord.Message = await channel.fetch_message(message_id)
        member: discord.Member = payload.member
        author: discord.Member = message.author
        if author == member:
            return

        mod: Optional[discord.Role] = discord.utils.get(member.roles,
                                                        name="Moderator")
        send_message: bool = False
        if mod_emoji_used:
            if not mod:
                return
            else:
                send_message = True
        else:
            reaction: discord.Reaction = next(r for r in message.reactions
                                              if str(r.emoji) == emoji.name)
            reaction_count: int = reaction.count
            if reaction_count > self.reaction_threshold:
                send_message = True
            elif reaction_count == self.reaction_threshold:
                send_message = True
                async for user in reaction.users():
                    if user == author:
                        send_message = False
                        break

        if send_message:
            HALL_OF_FAME_CHANNEL: discord.TextChannel = self.client.get_hof_channel(
                guild_id)
            embed = discord.Embed(color=discord.Color.red(),
                                  timestamp=message.created_at)
            embed.set_author(name=author, icon_url=author.avatar_url)
            attachments: List[discord.Attachment] = message.attachments
            if attachments:
                embed.set_image(url=attachments[0].proxy_url)
            message_content: str = message.content
            if message_content:
                if len(message_content) > 1024:
                    message_content = message_content[:1020] + "..."
                embed.add_field(name="Message", value=message_content)
            embed.add_field(name="Channel", value=channel.mention)
            embed.add_field(name="Jump To",
                            value=f"[Link]({message.jump_url})")
            embed.set_footer(text=f"Message ID: {message_id}")
            await HALL_OF_FAME_CHANNEL.send(embed=embed)
            self.client.db.add_message_to_hof(guild_id, message_id)
class CourseSelection(commands.Cog):
    """Handles the interaction surrounding selecting courses or other roles
    from course-regisration via commands and clean-up.

    Attributes
    ------------
    delete_self_message: `bool`
        Flag to determine whether to delete message sent by itself in the
        course-registration channel or not.
    """
    def __init__(self, client: commands.Bot):
        self.client = client
        self.delete_self_message: bool = True

    @commands.Cog.listener()
    async def on_guild_channel_update(self, before: discord.abc.GuildChannel,
                                      after: discord.abc.GuildChannel) -> None:
        """Updates course enrollment count when a member joins or leaves.

        Parameters
        -----------
        before: `discord.abc.GuildChannel`
            The channel before.
        after: `discord.abc.GuildChannel`
            The channel after.
        """
        if not isinstance(after, discord.TextChannel):
            return

        if before.topic is None or after.topic is None:
            return

        if not IS_COURSE_TOPIC.match(
                before.topic) or not IS_COURSE_TOPIC.match(after.topic):
            return

        if before.overwrites == after.overwrites:
            return

        enrolled_count: int = len(
            [x for x in after.overwrites if isinstance(x, discord.Member)])
        await after.edit(
            topic=re.sub(r"\(\d+", f"({enrolled_count}", after.topic))

    @commands.Cog.listener()
    async def on_message(self, message: discord.Message) -> None:
        """Deletes any message sent within 5 seconds in the course-registration channel.
        Will delete messages sent by itself when the delete_self_message flag is True,
        else it will not.

        Parameters
        -----------
        message: `discord.Message`
            The sent message.
        """
        channel: discord.TextChannel = message.channel
        if channel.id == COURSE_REGISTRATION_CHANNEL_ID:
            author: discord.Member = message.author
            admin: bool = author.permissions_in(channel).administrator
            if self.delete_self_message and author == self.client.user:
                await message.delete(delay=5)
            elif not admin and author != self.client.user:
                await message.delete(delay=5)

    @is_admin()
    @commands.guild_only()
    @commands.command(aliases=["toggleAD"])
    async def toggle_ad(self, ctx: commands.Context) -> None:
        """Toggles the delete_self_message flag."""
        self.delete_self_message = not self.delete_self_message
        await ctx.send(
            f"Deleting self-messages was toggled to: {self.delete_self_message}"
        )

    @commands.command()
    @commands.guild_only()
    @commands.check_any(in_channel(COURSE_REGISTRATION_CHANNEL_ID), is_admin())
    async def choose(self, ctx: commands.Context, *, name: str) -> None:
        """Command to add or remove any roles from the person invoking the command.
        If the author is not an admin, they can only toggle roles which are available
        in the course-registration channel.

        Parameters
        ------------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        name: `str`
            The name of the role/course to be toggled.
        """
        message: discord.Message = ctx.message
        guild: discord.Guild = ctx.guild
        author: discord.Member = message.author
        ADMIN_CHANNEL = guild.get_channel(ADMIN_CHANNEL_ID)
        await message.delete()

        course_channel: Optional[discord.TextChannel] = None
        try:
            course_channel = await CourseChannelConverter().convert(ctx, name)
        except commands.BadArgument as e:
            if "invalid course format" not in str(e):
                await ADMIN_CHANNEL.send(
                    f"{author.mention} just tried to add `{name}` using the `.choose` command. "
                    "Consider adding this course.")
                await ctx.send(
                    f"The course `{name}` is not available but I have notified the admin team to add it.",
                    delete_after=5,
                )
                return

        COURSE_REGISTRATION_CHANNEL: discord.TextChannel = guild.get_channel(
            COURSE_REGISTRATION_CHANNEL_ID)
        school_or_color: bool = False
        async for message in COURSE_REGISTRATION_CHANNEL.history(
                limit=4, oldest_first=True):
            if (f"({name.upper()})"
                    in message.content) or (f"-> {name.title()}"
                                            in message.content):
                school_or_color = True
                break
        if school_or_color:
            role: discord.Role = await CaseInsensitiveRoleConverter().convert(
                ctx, name)
            if role in author.roles:
                await author.remove_roles(role)
                await ctx.send(f"`{role.name}` has been removed.",
                               delete_after=5)
            else:
                await author.add_roles(role)
                await ctx.send(f"`{role.name}` has been added!",
                               delete_after=5)
        elif course_channel:
            overwrites: discord.PermissionOverwrite = course_channel.overwrites_for(
                author)
            if overwrites.is_empty():
                await course_channel.set_permissions(author,
                                                     read_messages=True,
                                                     send_messages=True)
                await ctx.send(
                    f"You have enrolled in `{course_channel.topic}`",
                    delete_after=5)
            else:
                await course_channel.set_permissions(author, overwrite=None)
                await ctx.send(
                    f"You have unenrolled in `{course_channel.topic}`",
                    delete_after=5,
                )
        else:
            await ctx.send(f"`{name}` is neither a toggleable role/course.",
                           delete_after=5)
Beispiel #8
0
class Stats(commands.Cog):
    def __init__(self, client: Bot):
        self.client = client

    @commands.command()
    @commands.guild_only()
    @commands.check_any(is_admin(), is_mod())
    async def serverinfo(self, ctx: commands.Context) -> None:
        """
        Sends an embedded message containing some stats about the server.
        Includes: Server ID, Server Owner, Region,
        Num of Channel Categories, Text Channels, Voice Channels, Roles, Members,
        Humans, Bots, Online/Idle/Dnd Members, Not Registered, New Accounts, Emojis,
        Verification Level, Active Invites, 2FA Status

        Parameters
        -----------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.

        Note: A New Account is considered to be an account which was created within 1 day of joining the server.
        """
        guild: discord.Guild = ctx.guild
        REGISTERED_ROLE: discord.Role = discord.utils.get(guild.roles,
                                                          name="Registered")
        new_accounts: int = Counter([(m.joined_at - m.created_at).days <= 1
                                     for m in guild.members])[True]
        not_registered_count: int = guild.member_count - len(
            REGISTERED_ROLE.members)
        num_bots: int = Counter([m.bot for m in guild.members])[True]
        statuses = Counter([(m.status, m.is_on_mobile())
                            for m in guild.members])
        online_mobile: int = statuses[(discord.Status.online, True)]
        idle_mobile: int = statuses[(discord.Status.idle, True)]
        dnd_mobile: int = statuses[(discord.Status.dnd, True)]
        online: int = statuses[(discord.Status.online, False)] + online_mobile
        idle: int = statuses[(discord.Status.idle, False)] + idle_mobile
        dnd: int = statuses[(discord.Status.dnd, False)] + dnd_mobile

        embed = discord.Embed(color=discord.Color.red(),
                              timestamp=guild.created_at)
        embed.set_author(name=guild, icon_url=guild.icon_url)
        embed.set_footer(text=f"Server ID: {guild.id} | Server Created")

        embed.add_field(name="Server Owner", value=guild.owner.mention)
        embed.add_field(name="Region", value=guild.region)
        embed.add_field(name="Channel Categories", value=len(guild.categories))
        embed.add_field(name="Text Channels", value=len(guild.text_channels))
        embed.add_field(name="Voice Channels", value=len(guild.voice_channels))
        embed.add_field(name="Roles", value=len(guild.roles))
        embed.add_field(name="Members", value=guild.member_count)
        embed.add_field(name="Humans", value=guild.member_count - num_bots)
        embed.add_field(name="Bots", value=num_bots)
        embed.add_field(name="Online",
                        value=f"{online} | Mobile: {online_mobile}")
        embed.add_field(name="Idle", value=f"{idle} | Mobile: {idle_mobile}")
        embed.add_field(name="Dnd", value=f"{dnd} | Mobile: {dnd_mobile}")
        embed.add_field(name="Not Registered", value=not_registered_count)
        embed.add_field(name="New Accounts", value=new_accounts)
        embed.add_field(name="Emojis",
                        value=f"{len(guild.emojis)}/{guild.emoji_limit}")
        embed.add_field(name="Verification Level",
                        value=guild.verification_level)
        embed.add_field(name="Active Invites",
                        value=len(await guild.invites()))
        embed.add_field(name="2FA", value=bool(guild.mfa_level))

        await ctx.send(embed=embed)

    @commands.command(aliases=[
        "orderedListMembers", "lsMembers", "listMembers", "list_members"
    ])
    @commands.guild_only()
    @commands.check_any(is_admin(), is_mod())
    async def ordered_list_members(self,
                                   ctx: commands.Context,
                                   num: int = 10,
                                   output_type: str = "nickname") -> None:
        """
        Sends an embedded message containing a list of members in order by
        when the joined the server.

        Parameters
        -----------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        num: `int`
            An integer representing the number of members to be displayed.
        output_type: `str`
            Specifies the format of the embedded message to display the users.
            Can be: "nickname", "nick", "name", "mention"
            Nickname will give a list of nicknames
            Name will give a list of usernames
            Mention will give a list of mentioned users.
        """
        msg: str = ""
        count: int = 0
        embed = discord.Embed(color=discord.Color.red(),
                              timestamp=datetime.utcnow())
        for member in sorted(ctx.guild.members, key=lambda m: m.joined_at):
            if count < num:
                if output_type == "nickname" or output_type == "nick":
                    msg += member.display_name + ", "
                elif output_type == "name":
                    msg += member.name + ", "
                elif output_type == "mention":
                    msg += member.mention + ", "
                else:
                    await ctx.send(
                        "Valid display type not given. Try: nickname/nick/name/mention"
                    )
                    return
                count += 1
                if count % 10 == 0:
                    embed.add_field(name=f"__{count - 9}-{count}:__",
                                    value=msg[:-2])
                    msg = ""
                if count % 100 == 0:
                    await ctx.send(embed=embed)
                    embed = discord.Embed(color=discord.Color.red(),
                                          timestamp=datetime.utcnow())
        # if an even 10 people was not reached
        if msg != "":
            embed.add_field(name=f"__{count - (count % 10) + 1}-{count}:__",
                            value=msg[:-2])
            await ctx.send(embed=embed)
        # if less than 100 members was reached
        elif msg == "":
            await ctx.send(embed=embed)

    @commands.command(aliases=["whoam"])
    @commands.guild_only()
    @commands.check_any(in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod())
    async def whois(self,
                    ctx: commands.Context,
                    *,
                    member_name: str = None) -> None:
        """
        Sends an embedded message containing information about the given user.

        Parameters
        -----------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        member_parts `Tuple`:
            The member's name as a tuple of strings.
        """
        member: Optional[discord.Member] = None
        if member_name is None or member_name.upper() == "I":
            member = ctx.author
        else:
            try:
                member = await FuzzyMemberConverter().convert(ctx, member_name)
            except discord.ext.commands.errors.BadArgument as e:
                await ctx.send(e)
                return

        member_permissions: discord.Permissions = ctx.author.permissions_in(
            ctx.channel)
        if (ctx.author == member or member_permissions.administrator
                or member_permissions.view_guild_insights):
            join_position: int = member_join_position(member)
            roles: str = member_mentioned_roles(member)
            permissions: str = ""
            for perm in member.guild_permissions:
                if perm[1]:
                    perm_name = perm[0].replace("_", " ").title()
                    permissions += perm_name + ", "
            permissions = permissions[:-2]

            embed = discord.Embed(
                color=member.color,
                timestamp=datetime.utcnow(),
                description=member.mention,
            )
            embed.set_thumbnail(url=member.avatar_url)
            embed.set_author(name=member, icon_url=member.avatar_url)
            embed.set_footer(text=f"Member ID: {member.id}")
            embed.add_field(name="Status", value=member.status)
            embed.add_field(name="Joined",
                            value=timestamp_format(member.joined_at))
            embed.add_field(name="Join Position", value=join_position)
            embed.add_field(name="Created At",
                            value=timestamp_format(member.created_at))
            embed.add_field(name=f"Roles ({len(member.roles) - 1})",
                            value=roles)
            embed.add_field(name="Key Permissions", value=permissions)

            await ctx.send(embed=embed)
        else:
            await ctx.message.delete()
            await ctx.send("Stop snooping around where you shouldn't 🙃",
                           delete_after=5)

    @commands.command(aliases=["joinNo", "joinPosition", "joinPos"])
    @commands.guild_only()
    @commands.check_any(is_admin(), is_mod())
    async def join_no(self, ctx: commands.Context, join_no: int) -> None:
        """
        Sends an embedded message containing information about the user who joined
        the server at the given join position.

        Parameters
        -----------
        ctx: `commands.Context`
            A class containing metadata about the command invocation.
        join_no: `int`
            The join position of the user to get information about.
        """
        guild: discord.Guild = ctx.guild
        if join_no <= 0:
            await ctx.send("Number must be a positive non-zero number.")
            return
        try:
            member: discord.Member = sorted(
                guild.members, key=lambda m: m.joined_at)[join_no - 1]
        except IndexError:
            await ctx.send(f"{guild} only has {guild.member_count} members!")
            return
        await self.whois(ctx, member.name)