Esempio n. 1
0
from random import seed, randint
from datetime import datetime

seed(datetime.now())

colours = [
    Colour.teal(),
    Colour.dark_teal(),
    Colour.green(),
    Colour.dark_green(),
    Colour.blue(),
    Colour.dark_blue(),
    Colour.purple(),
    Colour.dark_purple(),
    Colour.magenta(),
    Colour.dark_magenta(),
    Colour.gold(),
    Colour.dark_gold(),
    Colour.orange(),
    Colour.dark_orange(),
    Colour.red(),
    Colour.dark_red(),
    Colour.lighter_grey(),
    Colour.light_grey(),
    Colour.dark_grey(),
    Colour.darker_grey(),
    Colour.blurple(),
    Colour.greyple(),
    Colour.from_rgb(randint(0, 255), randint(0, 255), randint(0, 255))
]
Esempio n. 2
0
    async def clean_cancel(self, ctx: Context) -> None:
        """If there is an ongoing cleaning process, attempt to immediately cancel it."""
        self.cleaning = False

        embed = Embed(color=Colour.blurple(), description="Clean interrupted.")
        await ctx.send(embed=embed, delete_after=10)
Esempio n. 3
0
    async def guildinfo(self, ctx: Context, *, guild_id: int = None) -> None:
        """Returns info about a guild"""
        if guild_id is not None and await self.bot.is_owner(ctx.author):
            guild = self.bot.get_guild(guild_id)
            if guild is None:
                return await ctx.send(f'Invalid Guild ID given.')
        else:
            guild = ctx.guild
        
        created = guild.created_at
        features = ", ".join(guild.features)

        id = guild.id
        owner = guild.owner
        ownerdn = guild.owner.display_name
        
        boostlvl = guild.premium_tier
        boostlen = guild.premium_subscription_count
       
        #lists of server elements
        rolelist = guild.roles
        cats = guild.categories
        chans = guild.text_channels
        
        #Dictionary of flags
        regionFlag = {
            'amsterdam': ":flag_nl: - Amsterdam",
            'brazil': ":flag_br: -  Brazil",
            'eu_central': ":flag_eu: - Central Europe",
            'eu-central': "",
            'eu_west': ":flag_eu: - West Europe",
            'eu-west': "",
            'europe': ":flag_eu: - Europe",
            'frankfurt': ":flag_de: - Frankfurt",
            'hongkong': ":flag_ch: - Hong Kong",
            'india': ":flag_in: - India",
            'japan': ":flag_jap: - Japan",
            'london': ":flag_uk: - London",
            'russia': ":flag_ru: - Russia",
            'singapore': ":flag_au: - Singapore",
            'southafrica': ":flag_za: - South Africa",
            'sydney': ":flag_au: - Sydney",
            'us_central': ":flag_us: - US Central",
            'us-central': "",
            'us_east': ":flag_us: - US East",
            'us-east': "",
            'us_south': ":flag_us: - US South",
            'us-south': "",
            'us_west': ":flag_us: - US West",
            'us-west': ""
        }

        region = guild.region
        
        embed = discord.Embed(title=str(guild.name) + "'s information", colour=Colour.blurple())
        embed.add_field(name=":id:", value=id)
        embed.add_field(name=":date: Guild Created On", value=created.strftime("%A %d %B %Y %H:%M"))
        embed.add_field(name=":bust_in_silhouette: Owner", value=str(owner) + " aka " + str(ownerdn))
        embed.add_field(name=":telephone_receiver: Voice Region", value=" ".join([regionFlag[n] for n in region]))
        #embed.add_field(name=":telephone_receiver: Voice Region", value=region)
        embed.add_field(name="Nitro Level", value=str(boostlvl) + "/" + str(3))
        embed.add_field(name="# of current boosts", value=str(boostlen) + "/" + str(30))
        if boostlen > 2:
            embed.add_field(name=".. needed for lvl 1", value="Already unlocked")
        else:
            embed.add_field(name=".. needed for lvl 1", value=str(2 - boostlen))
        if boostlen > 15:
            embed.add_field(name=".. needed for lvl 2", value="Already unlocked")
        else:    
            embed.add_field(name=".. needed for lvl 2", value=str(15 - boostlen))
        if boostlen > 30:
            embed.add_field(name=".. needed for lvl 3", value="Already unlocked")
        else:
            embed.add_field(name=".. needed for lvl 3", value=str(30 - boostlen))
        
        embed.set_thumbnail(url=guild.icon_url)

        if boostlen > 30:
            embed.set_footer(text="Max Level reached", icon_url="")
        else:
            embed.set_footer(text=str(30 - boostlen) + " boosts to go for max boost level", icon_url="")
                   
        await ctx.send(embed=embed)
Esempio n. 4
0
            fields.append(await self.expanded_user_infraction_counts(user))
            fields.append(await self.user_nomination_counts(user))
        else:
            fields.append(await self.basic_user_infraction_counts(user))

        # Let's build the embed now
        embed = Embed(
            title=name,
            description=" ".join(badges)
        )

        for field_name, field_content in fields:
            embed.add_field(name=field_name, value=field_content, inline=False)

        embed.set_thumbnail(url=user.avatar_url_as(static_format="png"))
        embed.colour = user.colour if user.colour != Colour.default() else Colour.blurple()

        return embed

    async def basic_user_infraction_counts(self, user: FetchedMember) -> Tuple[str, str]:
        """Gets the total and active infraction counts for the given `member`."""
        infractions = await self.bot.api_client.get(
            'bot/infractions',
            params={
                'hidden': 'False',
                'user__id': str(user.id)
            }
        )

        total_infractions = len(infractions)
        active_infractions = sum(infraction['active'] for infraction in infractions)
Esempio n. 5
0
    async def on_message_edit(self, msg_before: discord.Message,
                              msg_after: discord.Message) -> None:
        """Log message edit event to message change log."""
        if (not msg_before.guild or msg_before.guild.id != GuildConstant.id
                or msg_before.channel.id in GuildConstant.ignored
                or msg_before.author.bot):
            return

        self._cached_edits.append(msg_before.id)

        if msg_before.content == msg_after.content:
            return

        author = msg_before.author
        channel = msg_before.channel
        channel_name = f"{channel.category}/#{channel.name}" if channel.category else f"#{channel.name}"

        # Getting the difference per words and group them by type - add, remove, same
        # Note that this is intended grouping without sorting
        diff = difflib.ndiff(msg_before.clean_content.split(),
                             msg_after.clean_content.split())
        diff_groups = tuple(
            (diff_type, tuple(s[2:] for s in diff_words))
            for diff_type, diff_words in itertools.groupby(diff,
                                                           key=lambda s: s[0]))

        content_before: t.List[str] = []
        content_after: t.List[str] = []

        for index, (diff_type, words) in enumerate(diff_groups):
            sub = ' '.join(words)
            if diff_type == '-':
                content_before.append(f"[{sub}](http://o.hi)")
            elif diff_type == '+':
                content_after.append(f"[{sub}](http://o.hi)")
            elif diff_type == ' ':
                if len(words) > 2:
                    sub = (
                        f"{words[0] if index > 0 else ''}"
                        " ... "
                        f"{words[-1] if index < len(diff_groups) - 1 else ''}")
                content_before.append(sub)
                content_after.append(sub)

        response = (f"**Author:** {author} (`{author.id}`)\n"
                    f"**Channel:** {channel_name} (`{channel.id}`)\n"
                    f"**Message ID:** `{msg_before.id}`\n"
                    "\n"
                    f"**Before**:\n{' '.join(content_before)}\n"
                    f"**After**:\n{' '.join(content_after)}\n"
                    "\n"
                    f"[Jump to message]({msg_after.jump_url})")

        if msg_before.edited_at:
            # Message was previously edited, to assist with self-bot detection, use the edited_at
            # datetime as the baseline and create a human-readable delta between this edit event
            # and the last time the message was edited
            timestamp = msg_before.edited_at
            delta = humanize_delta(
                relativedelta(msg_after.edited_at, msg_before.edited_at))
            footer = f"Last edited {delta} ago"
        else:
            # Message was not previously edited, use the created_at datetime as the baseline, no
            # delta calculation needed
            timestamp = msg_before.created_at
            footer = None

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited",
                                    response,
                                    channel_id=Channels.message_log,
                                    timestamp_override=timestamp,
                                    footer=footer)
Esempio n. 6
0
    async def user_info(self,
                        ctx: Context,
                        user: Member = None,
                        hidden: bool = False):
        """
        Returns info about a user.
        """

        # Do a role check if this is being executed on
        # someone other than the caller
        if user and user != ctx.author:
            if not with_role_check(ctx, *MODERATION_ROLES):
                raise BadArgument(
                    "You do not have permission to use this command on users other than yourself."
                )

        # Non-moderators may only do this in #bot-commands and can't see
        # hidden infractions.
        if not with_role_check(ctx, *STAFF_ROLES):
            if not ctx.channel.id == Channels.bot:
                raise MissingPermissions("You can't do that here!")
            # Hide hidden infractions for users without a moderation role
            hidden = False

        # Validates hidden input
        hidden = str(hidden)

        if user is None:
            user = ctx.author

        # User information
        created = time_since(user.created_at, max_units=3)

        name = f"{user.name}#{user.discriminator}"
        if user.nick:
            name = f"{user.nick} ({name})"

        # Member information
        joined = time_since(user.joined_at, precision="days")

        # You're welcome, Volcyyyyyyyyyyyyyyyy
        roles = ", ".join(role.mention for role in user.roles
                          if role.name != "@everyone")

        # Infractions
        api_response = await self.bot.http_session.get(
            url=URLs.site_infractions_user.format(user_id=user.id),
            params={"hidden": hidden},
            headers=self.headers)

        infractions = await api_response.json()

        infr_total = 0
        infr_active = 0

        # At least it's readable.
        for infr in infractions:
            if infr["active"]:
                infr_active += 1

            infr_total += 1

        # Let's build the embed now
        embed = Embed(title=name,
                      description=textwrap.dedent(f"""
                **User Information**
                Created: {created}
                Profile: {user.mention}
                ID: {user.id}

                **Member Information**
                Joined: {joined}
                Roles: {roles or None}

                **Infractions**
                Total: {infr_total}
                Active: {infr_active}
            """))

        embed.set_thumbnail(url=user.avatar_url_as(format="png"))
        embed.colour = user.top_role.colour if roles else Colour.blurple()

        await ctx.send(embed=embed)
Esempio n. 7
0
class Information(Cog):
    """A cog with commands for generating embeds with server info, such as server stats and user info."""
    def __init__(self, bot: Bot):
        self.bot = bot

    @staticmethod
    def role_can_read(channel: GuildChannel, role: Role) -> bool:
        """Return True if `role` can read messages in `channel`."""
        overwrites = channel.overwrites_for(role)
        return overwrites.read_messages is True

    def get_staff_channel_count(self, guild: Guild) -> int:
        """
        Get the number of channels that are staff-only.

        We need to know two things about a channel:
        - Does the @everyone role have explicit read deny permissions?
        - Do staff roles have explicit read allow permissions?

        If the answer to both of these questions is yes, it's a staff channel.
        """
        channel_ids = set()
        for channel in guild.channels:
            if channel.type is ChannelType.category:
                continue

            everyone_can_read = self.role_can_read(channel, guild.default_role)

            for role in constants.STAFF_ROLES:
                role_can_read = self.role_can_read(channel,
                                                   guild.get_role(role))
                if role_can_read and not everyone_can_read:
                    channel_ids.add(channel.id)
                    break

        return len(channel_ids)

    @staticmethod
    def get_channel_type_counts(guild: Guild) -> str:
        """Return the total amounts of the various types of channels in `guild`."""
        channel_counter = Counter(c.type for c in guild.channels)
        channel_type_list = []
        for channel, count in channel_counter.items():
            channel_type = str(channel).title()
            channel_type_list.append(f"{channel_type} channels: {count}")

        channel_type_list = sorted(channel_type_list)
        return "\n".join(channel_type_list)

    @has_any_role(*constants.STAFF_ROLES)
    @command(name="roles")
    async def roles_info(self, ctx: Context) -> None:
        """Returns a list of all roles and their corresponding IDs."""
        # Sort the roles alphabetically and remove the @everyone role
        roles = sorted(ctx.guild.roles[1:], key=lambda role: role.name)

        # Build a list
        role_list = []
        for role in roles:
            role_list.append(f"`{role.id}` - {role.mention}")

        # Build an embed
        embed = Embed(
            title=
            f"Role information (Total {len(roles)} role{'s' * (len(role_list) > 1)})",
            colour=Colour.blurple())

        await LinePaginator.paginate(role_list, ctx, embed, empty=False)

    @has_any_role(*constants.STAFF_ROLES)
    @command(name="role")
    async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None:
        """
        Return information on a role or list of roles.

        To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks.
        """
        parsed_roles = set()
        failed_roles = set()

        all_roles = {role.id: role.name for role in ctx.guild.roles}
        for role_name in roles:
            if isinstance(role_name, Role):
                # Role conversion has already succeeded
                parsed_roles.add(role_name)
                continue

            match = fuzzywuzzy.process.extractOne(role_name,
                                                  all_roles,
                                                  score_cutoff=80,
                                                  scorer=fuzzywuzzy.fuzz.ratio)

            if not match:
                failed_roles.add(role_name)
                continue

            # `match` is a (role name, score, role id) tuple
            role = ctx.guild.get_role(match[2])
            parsed_roles.add(role)

        if failed_roles:
            await ctx.send(
                f":x: Could not retrieve the following roles: {', '.join(failed_roles)}"
            )

        for role in parsed_roles:
            h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb())

            embed = Embed(
                title=f"{role.name} info",
                colour=role.colour,
            )
            embed.add_field(name="ID", value=role.id, inline=True)
            embed.add_field(name="Colour (RGB)",
                            value=f"#{role.colour.value:0>6x}",
                            inline=True)
            embed.add_field(name="Colour (HSV)",
                            value=f"{h:.2f} {s:.2f} {v}",
                            inline=True)
            embed.add_field(name="Member count",
                            value=len(role.members),
                            inline=True)
            embed.add_field(name="Position", value=role.position)
            embed.add_field(name="Permission code",
                            value=role.permissions.value,
                            inline=True)

            await ctx.send(embed=embed)

    @command(name="server", aliases=["server_info", "guild", "guild_info"])
    async def server_info(self, ctx: Context) -> None:
        """Returns an embed full of server information."""
        created = time_since(ctx.guild.created_at, precision="days")
        features = ", ".join(ctx.guild.features)
        region = ctx.guild.region

        roles = len(ctx.guild.roles)
        member_count = ctx.guild.member_count
        channel_counts = self.get_channel_type_counts(ctx.guild)

        # How many of each user status?
        py_invite = await self.bot.fetch_invite(constants.Guild.invite)
        online_presences = py_invite.approximate_presence_count
        offline_presences = py_invite.approximate_member_count - online_presences
        embed = Embed(colour=Colour.blurple())

        # How many staff members and staff channels do we have?
        staff_member_count = len(
            ctx.guild.get_role(constants.Roles.helpers).members)
        staff_channel_count = self.get_staff_channel_count(ctx.guild)

        # Because channel_counts lacks leading whitespace, it breaks the dedent if it's inserted directly by the
        # f-string. While this is correctly formatted by Discord, it makes unit testing difficult. To keep the
        # formatting without joining a tuple of strings we can use a Template string to insert the already-formatted
        # channel_counts after the dedent is made.
        embed.description = Template(
            textwrap.dedent(f"""
                **Server information**
                Created: {created}
                Voice region: {region}
                Features: {features}

                **Channel counts**
                $channel_counts
                Staff channels: {staff_channel_count}

                **Member counts**
                Members: {member_count:,}
                Staff members: {staff_member_count}
                Roles: {roles}

                **Member statuses**
                {constants.Emojis.status_online} {online_presences:,}
                {constants.Emojis.status_offline} {offline_presences:,}
            """)).substitute({"channel_counts": channel_counts})
        embed.set_thumbnail(url=ctx.guild.icon_url)

        await ctx.send(embed=embed)

    @command(name="user", aliases=["user_info", "member", "member_info"])
    async def user_info(self,
                        ctx: Context,
                        user: FetchedMember = None) -> None:
        """Returns info about a user."""
        if user is None:
            user = ctx.author

        # Do a role check if this is being executed on someone other than the caller
        elif user != ctx.author and await has_no_roles_check(
                ctx, *constants.MODERATION_ROLES):
            await ctx.send(
                "You may not use this command on users other than yourself.")
            return

        # Will redirect to #bot-commands if it fails.
        if in_whitelist_check(ctx, roles=constants.STAFF_ROLES):
            embed = await self.create_user_embed(ctx, user)
            await ctx.send(embed=embed)

    async def create_user_embed(self, ctx: Context,
                                user: FetchedMember) -> Embed:
        """Creates an embed containing information on the `user`."""
        on_server = bool(ctx.guild.get_member(user.id))

        created = time_since(user.created_at, max_units=3)

        name = str(user)
        if on_server and user.nick:
            name = f"{user.nick} ({name})"

        badges = []

        for badge, is_set in user.public_flags:
            if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}",
                                            None)):
                badges.append(emoji)

        activity = await self.user_messages(user)

        if on_server:
            joined = time_since(user.joined_at, max_units=3)
            roles = ", ".join(role.mention for role in user.roles[1:])
            membership = {
                "Joined": joined,
                "Verified": not user.pending,
                "Roles": roles or None
            }
            if not is_mod_channel(ctx.channel):
                membership.pop("Verified")

            membership = textwrap.dedent("\n".join(
                [f"{key}: {value}" for key, value in membership.items()]))
        else:
            roles = None
            membership = "The user is not a member of the server"

        fields = [
            ("User information",
             textwrap.dedent(f"""
                    Created: {created}
                    Profile: {user.mention}
                    ID: {user.id}
                """).strip()),
            ("Member information", membership),
        ]

        # Show more verbose output in moderation channels for infractions and nominations
        if is_mod_channel(ctx.channel):
            fields.append(activity)

            fields.append(await self.expanded_user_infraction_counts(user))
            fields.append(await self.user_nomination_counts(user))
        else:
            fields.append(await self.basic_user_infraction_counts(user))

        # Let's build the embed now
        embed = Embed(title=name, description=" ".join(badges))

        for field_name, field_content in fields:
            embed.add_field(name=field_name, value=field_content, inline=False)

        embed.set_thumbnail(url=user.avatar_url_as(static_format="png"))
        embed.colour = user.top_role.colour if roles else Colour.blurple()

        return embed
Esempio n. 8
0
 async def help_(self, ctx, second_help: str = None):
     cogs = sorted([
         cog for cog in self.bot.cogs.keys()
         if cog not in ['ErrorHandler', 'Tavern', 'Events']
     ])
     pages = []
     page = 1
     cmd_names = [cmd.name for cmd in self.bot.commands]
     if not second_help:
         for cog_name in cogs:
             cog = self.bot.get_cog(cog_name)
             commands = [
                 cmd for cmd in cog.get_commands()
                 if not cmd.hidden or cmd.name == 'help'
             ]
             message = cog.description + '\n'
             for cmd in commands:
                 if cmd.name == 'subreddit':
                     for sub_cmd in cmd.walk_commands():
                         message += f' \n  **{self.config["prefix"]}{sub_cmd}** \n *{sub_cmd.help}*'
                 else:
                     message += f' \n  **{self.config["prefix"]}{cmd}** \n *{cmd.help}*'
             help_embed = Embed(title=cog_name,
                                colour=Colour.blurple(),
                                description=message)
             help_embed.set_footer(text=f'Page: {page}/{len(cogs)}')
             help_embed.set_author(name=f'{ctx.author}',
                                   icon_url=ctx.author.avatar_url)
             pages.append(help_embed)
             page = page + 1
         embed = Paginator(embed=False,
                           timeout=90,
                           use_defaults=True,
                           extra_pages=pages,
                           length=1)
         await embed.start(ctx)
     else:
         if second_help.lower() in cmd_names:
             cmd = self.bot.get_command(second_help)
             embed = Embed(title=cmd.name, colour=Colour.blurple())
             value = ''
             if cmd.aliases:
                 for alias in cmd.aliases:
                     value += f'{str(alias)}, '
                 value = value[0:-2]
                 value = value + '.'
             else:
                 value = None
             embed.add_field(name="Aliases",
                             value=f'*{value}*',
                             inline=False)
             params_list = list(cmd.params.keys())
             req_params = []
             for value in params_list:
                 req_params.append(value)
             req_params.remove('self')
             req_params.remove('ctx')
             param_message = 'Required parameters are:\n**'
             if req_params:
                 for parm in req_params:
                     param_message += parm + '\n'
                 embed.add_field(name='Usage',
                                 value=param_message + '**',
                                 inline=False)
             else:
                 embed.add_field(name='Usage',
                                 value=param_message + 'None**',
                                 inline=False)
             embed.set_author(name=f'{ctx.author}',
                              icon_url=ctx.author.avatar_url)
             return await ctx.send(embed=embed)
         else:
             return await ctx.send(
                 f"{str(second_help)} command does not exist!")
Esempio n. 9
0
            fields.append(await self.expanded_user_infraction_counts(user))
            fields.append(await self.user_nomination_counts(user))
        else:
            fields.append(await self.basic_user_infraction_counts(user))

        # Let's build the embed now
        embed = Embed(
            title=name,
            description=" ".join(badges)
        )

        for field_name, field_content in fields:
            embed.add_field(name=field_name, value=field_content, inline=False)

        embed.set_thumbnail(url=user.avatar_url_as(static_format="png"))
        embed.colour = user.top_role.colour if roles else Colour.blurple()

        return embed

    async def basic_user_infraction_counts(self, user: FetchedMember) -> Tuple[str, str]:
        """Gets the total and active infraction counts for the given `member`."""
        infractions = await self.bot.api_client.get(
            'bot/infractions',
            params={
                'hidden': 'False',
                'user__id': str(user.id)
            }
        )

        total_infractions = len(infractions)
        active_infractions = sum(infraction['active'] for infraction in infractions)
Esempio n. 10
0
File: reddit.py Progetto: gsvolt/bot
    async def poll_new_posts(self) -> None:
        """Periodically search for new subreddit posts."""
        while True:
            await asyncio.sleep(RedditConfig.request_delay)

            for subreddit in RedditConfig.subreddits:
                # Make a HEAD request to the subreddit
                head_response = await self.bot.http_session.head(
                    url=f"{self.URL}/{subreddit}/new.rss",
                    headers=self.HEADERS)

                content_length = head_response.headers["content-length"]

                # If the content is the same size as before, assume there's no new posts.
                if content_length == self.prev_lengths.get(subreddit, None):
                    continue

                self.prev_lengths[subreddit] = content_length

                # Now we can actually fetch the new data
                posts = await self.fetch_posts(f"{subreddit}/new")
                new_posts = []

                # Only show new posts if we've checked before.
                if subreddit in self.last_ids:
                    for post in posts:
                        data = post["data"]

                        # Convert the ID to an integer for easy comparison.
                        int_id = int(data["id"], 36)

                        # If we've already seen this post, finish checking
                        if int_id <= self.last_ids[subreddit]:
                            break

                        embed_data = {
                            "title":
                            textwrap.shorten(data["title"],
                                             width=64,
                                             placeholder="..."),
                            "text":
                            textwrap.shorten(data["selftext"],
                                             width=128,
                                             placeholder="..."),
                            "url":
                            self.URL + data["permalink"],
                            "author":
                            data["author"]
                        }

                        new_posts.append(embed_data)

                self.last_ids[subreddit] = int(posts[0]["data"]["id"], 36)

                # Send all of the new posts as spicy embeds
                for data in new_posts:
                    embed = Embed()

                    embed.title = data["title"]
                    embed.url = data["url"]
                    embed.description = data["text"]
                    embed.set_footer(
                        text=f"Posted by u/{data['author']} in {subreddit}")
                    embed.colour = Colour.blurple()

                    await self.reddit_channel.send(embed=embed)

                log.trace(
                    f"Sent {len(new_posts)} new {subreddit} posts to channel {self.reddit_channel.id}."
                )
Esempio n. 11
0
    async def on_message_edit(self, before: discord.Message,
                              after: discord.Message) -> None:
        """Log message edit event to modlog."""
        if (not before.guild or before.guild.id != GuildConstant.id
                or before.channel.id in GuildConstant.ignored
                or before.author.bot):
            return

        self._cached_edits.append(before.id)

        if before.content == after.content:
            return

        author = before.author
        channel = before.channel

        if channel.category:
            before_response = (
                f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{before.clean_content}")

            after_response = (
                f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{after.clean_content}")
        else:
            before_response = (
                f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{before.clean_content}")

            after_response = (
                f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{after.clean_content}")

        if before.edited_at:
            timestamp = before.edited_at
            delta = humanize_delta(relativedelta(after.edited_at, before.edit))
            footer = f"Last edited {delta} ago"
        else:
            timestamp = before.created_at
            footer = None

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (Before)",
                                    before_response,
                                    channel_id=Channels.modlog,
                                    timestamp_override=timestamp,
                                    footer=footer)

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (After)",
                                    after_response,
                                    channel_id=Channels.modlog,
                                    timestamp_override=timestamp,
                                    footer=footer)
Esempio n. 12
0
    async def tasks_command(self,
                            ctx: Context,
                            status: str = None,
                            task_list: str = None):
        """
        Get a list of tasks, optionally on a specific list or with a specific status

        Provide "*" for the status to match everything except for "Closed".

        When specifying a list you may use the list name on its own, but it is preferable to give the project name
        as well - for example, "Bot/Cogs". This is case-insensitive.
        """

        params = {}

        embed = Embed(colour=Colour.blurple())
        embed.set_author(
            name="ClickUp Tasks",
            icon_url="https://clickup.com/landing/favicons/favicon-32x32.png",
            url=f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/")

        if task_list:
            if task_list in self.lists:
                params["list_ids[]"] = self.lists[task_list]
            else:
                log.warning(
                    f"{ctx.author} requested '{task_list}', but that list is unknown. Rejecting request."
                )
                embed.description = f"Unknown list: {task_list}"
                embed.colour = Colour.red()
                return await ctx.send(embed=embed)

        if status and status != "*":
            params["statuses[]"] = status

        response = await self.bot.http_session.get(
            GET_TASKS_URL.format(team_id=CLICKUP_TEAM),
            headers=HEADERS,
            params=params)
        result = await response.json()

        if "err" in result:
            log.error(
                "ClickUp responded to the task list request with an error!\n"
                f"error code: '{result['ECODE']}'\n"
                f"error: {result['err']}")
            embed.description = f"`{result['ECODE']}`: {result['err']}"
            embed.colour = Colour.red()

        else:
            tasks = result["tasks"]

            if not tasks:
                log.debug(
                    f"{ctx.author} requested a list of ClickUp tasks, but no ClickUp tasks were found."
                )
                embed.description = "No tasks found."
                embed.colour = Colour.red()

            else:
                lines = []

                for task in tasks:
                    task_url = f"http://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/t/{task['id']}"
                    id_fragment = f"[`#{task['id']: <5}`]({task_url})"
                    status = f"{task['status']['status'].title()}"

                    lines.append(
                        f"{id_fragment} ({status})\n\u00BB {task['name']}")

                log.debug(
                    f"{ctx.author} requested a list of ClickUp tasks. Returning list."
                )
                return await LinePaginator.paginate(lines,
                                                    ctx,
                                                    embed,
                                                    max_size=750)
        return await ctx.send(embed=embed)
Esempio n. 13
0
    async def task_command(self, ctx: Context, task_id: str):
        """
        Get a task and return information specific to it
        """

        if task_id.startswith("#"):
            task_id = task_id[1:]

        embed = Embed(colour=Colour.blurple())
        embed.set_author(
            name=f"ClickUp Task: #{task_id}",
            icon_url="https://clickup.com/landing/favicons/favicon-32x32.png",
            url=
            f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/t/{task_id}"
        )

        params = MultiDict()
        params.add("statuses[]", "Open")
        params.add("statuses[]", "in progress")
        params.add("statuses[]", "review")
        params.add("statuses[]", "Closed")

        response = await self.bot.http_session.get(
            GET_TASKS_URL.format(team_id=CLICKUP_TEAM),
            headers=HEADERS,
            params=params)
        result = await response.json()

        if "err" in result:
            log.error(
                "ClickUp responded to the get task request with an error!\n"
                f"error code: '{result['ECODE']}'\n"
                f"error: {result['err']}")
            embed.description = f"`{result['ECODE']}`: {result['err']}"
            embed.colour = Colour.red()
        else:
            task = None

            for task_ in result["tasks"]:
                if task_["id"] == task_id:
                    task = task_
                    break

            if task is None:
                log.warning(
                    f"{ctx.author} requested the task '#{task_id}', but it could not be found."
                )
                embed.description = f"Unable to find task with ID `#{task_id}`:"
                embed.colour = Colour.red()
            else:
                status = task['status']['status'].title()
                project, list_ = self.lists[task['list']['id']].split("/", 1)
                list_ = f"{project.title()}/{list_.title()}"
                first_line = f"**{list_}** \u00BB *{task['name']}* \n**Status**: {status}"

                if task.get("tags"):
                    tags = ", ".join(tag["name"].title()
                                     for tag in task["tags"])
                    first_line += f" / **Tags**: {tags}"

                lines = [first_line]

                if task.get("text_content"):
                    text = task["text_content"]

                    if len(text) >= 1500:
                        text = text[:1497] + "..."

                    lines.append(text)

                if task.get("assignees"):
                    assignees = ", ".join(user["username"]
                                          for user in task["assignees"])
                    lines.append(f"**Assignees**\n{assignees}")

                log.debug(
                    f"{ctx.author} requested the task '#{task_id}'. Returning the task data."
                )
                return await LinePaginator.paginate(lines,
                                                    ctx,
                                                    embed,
                                                    max_size=1500)
        return await ctx.send(embed=embed)
Esempio n. 14
0
    async def on_guild_channel_update(self, before: GUILD_CHANNEL,
                                      after: GuildChannel) -> None:
        """Log channel update event to mod log."""
        if before.guild.id != GuildConstant.id:
            return

        if before.id in self._ignored[Event.guild_channel_update]:
            self._ignored[Event.guild_channel_update].remove(before.id)
            return

        # Two channel updates are sent for a single edit: 1 for topic and 1 for category change.
        # TODO: remove once support is added for ignoring multiple occurrences for the same channel.
        help_categories = (Categories.help_available, Categories.help_dormant,
                           Categories.help_in_use)
        if after.category and after.category.id in help_categories:
            return

        diff = DeepDiff(before, after)
        changes = []
        done = []

        diff_values = diff.get("values_changed", {})
        diff_values.update(diff.get("type_changes", {}))

        for key, value in diff_values.items():
            if not key:  # Not sure why, but it happens
                continue

            key = key[5:]  # Remove "root." prefix

            if "[" in key:
                key = key.split("[", 1)[0]

            if "." in key:
                key = key.split(".", 1)[0]

            if key in done or key in CHANNEL_CHANGES_SUPPRESSED:
                continue

            if key in CHANNEL_CHANGES_UNSUPPORTED:
                changes.append(f"**{key.title()}** updated")
            else:
                new = value["new_value"]
                old = value["old_value"]

                # Discord does not treat consecutive backticks ("``") as an empty inline code block, so the markdown
                # formatting is broken when `new` and/or `old` are empty values. "None" is used for these cases so
                # formatting is preserved.
                changes.append(
                    f"**{key.title()}:** `{old or 'None'}` **→** `{new or 'None'}`"
                )

            done.append(key)

        if not changes:
            return

        message = ""

        for item in sorted(changes):
            message += f"{Emojis.bullet} {item}\n"

        if after.category:
            message = f"**{after.category}/#{after.name} (`{after.id}`)**\n{message}"
        else:
            message = f"**#{after.name}** (`{after.id}`)\n{message}"

        await self.send_log_message(Icons.hash_blurple, Colour.blurple(),
                                    "Channel updated", message)
Esempio n. 15
0
    async def character_cmd(self, ctx):
        """View your character or create one."""
        db_connection = await dbconnection()
        cursor = await db_connection.cursor()
        user_id = ctx.author.id
        channel = ctx.channel
        select_characters = "SELECT * FROM `character` WHERE user_id = '%s'"
        val = user_id
        await cursor.execute(select_characters, (val, ))
        result = await cursor.fetchall()
        if is_empty(result) is True:
            await ctx.send(
                "Starting character creation process, what do you want your character to be called?"
            )
            try:

                def check(m):
                    return m.channel == channel and ctx.author == m.author

                msg = await self.bot.wait_for('message',
                                              timeout=15.0,
                                              check=check)
            except asyncio.TimeoutError:
                return await ctx.send(
                    'No response. Character creation stopped.')
            character_name = msg.content
            await ctx.send('Give me a short background on your character.')
            try:

                def check(m):
                    return m.channel == channel and ctx.author == m.author

                msg = await self.bot.wait_for('message',
                                              timeout=120.0,
                                              check=check)
            except asyncio.TimeoutError:
                return await ctx.send(
                    'No response. Character creation stopped.')
            character_description = msg.content
            # insert the data the user has submitted
            sql = "INSERT INTO `character`(`name`, user_id, description) VALUES(%s, %s, %s)"
            val = (character_name, user_id, character_description)
            await cursor.execute(sql, val)
            # insert the standard data that is set on default
            sql = "INSERT INTO inventory(character_id, gold) VALUES(%s, %s)"
            val = (user_id, 100)
            await cursor.execute(sql, val)
            sql = "INSERT INTO jobs(character_id, job) VALUES(%s, %s)"
            val = (user_id, 'miner')
            await cursor.execute(sql, val)
            try:
                await db_connection.commit()
                await cursor.close()
                db_connection.close()
                return await ctx.send(
                    'Succesfully created your character! Use !character to acces it.'
                )
            except:
                await cursor.close()
                db_connection.close()
                return await ctx.send(
                    'Could not create your character. Something went wrong.')
        else:
            pages = []
            select_character = "SELECT * FROM `character` " \
                               "LEFT JOIN inventory ON `character`.user_id = inventory.character_id " \
                               "LEFT JOIN jobs ON inventory.character_id  = jobs.character_id " \
                               "AND jobs.current_job = %s " \
                               "WHERE `character`.user_id = '%s'"
            val = ('true', user_id)
            await cursor.execute(select_character, val)
            results = await cursor.fetchall()
            columns = [desc[0] for desc in cursor.description]
            result = []
            for row in results:
                row = dict(zip(columns, row))
                result.append(row)
            embed = Embed(title=result[0]['name'],
                          colour=Colour.blurple(),
                          description=result[0]['description'])
            embed.add_field(name="Details",
                            value=f"**Current Job:** {result[0]['job']} \n")
            ores = ['stone', 'coal', 'iron', 'ruby']
            desc = ''
            for x in ores:
                if result[0][x] > 0:
                    desc += f"**{x}:** {result[0][x]} \n"
            embed.add_field(name='Inventory',
                            value=f"**Gold:** {result[0]['gold']}\n{desc}",
                            inline=False)
            pages.append(embed)
            desc = f"**Skill Shards:** {result[0]['skillshard']}"
            embed = Embed(title="Skills",
                          colour=Colour.blurple(),
                          description=desc)
            pages.append(embed)
            await cursor.close()
            db_connection.close()
            embed = Paginator(embed=False,
                              timeout=90,
                              use_defaults=True,
                              extra_pages=pages,
                              length=1)
            await embed.start(ctx)
Esempio n. 16
0
    async def on_voice_state_update(self, member: discord.Member,
                                    before: discord.VoiceState,
                                    after: discord.VoiceState) -> None:
        """Log member voice state changes to the voice log channel."""
        if (member.guild.id != GuildConstant.id
                or (before.channel
                    and before.channel.id in GuildConstant.modlog_blacklist)):
            return

        if member.id in self._ignored[Event.voice_state_update]:
            self._ignored[Event.voice_state_update].remove(member.id)
            return

        # Exclude all channel attributes except the name.
        diff = DeepDiff(
            before,
            after,
            exclude_paths=("root.session_id", "root.afk"),
            exclude_regex_paths=r"root\.channel\.(?!name)",
        )

        # A type change seems to always take precedent over a value change. Furthermore, it will
        # include the value change along with the type change anyway. Therefore, it's OK to
        # "overwrite" values_changed; in practice there will never even be anything to overwrite.
        diff_values = {
            **diff.get("values_changed", {}),
            **diff.get("type_changes", {})
        }

        icon = Icons.voice_state_blue
        colour = Colour.blurple()
        changes = []

        for attr, values in diff_values.items():
            if not attr:  # Not sure why, but it happens.
                continue

            old = values["old_value"]
            new = values["new_value"]

            attr = attr[5:]  # Remove "root." prefix.
            attr = VOICE_STATE_ATTRIBUTES.get(
                attr,
                attr.replace("_", " ").capitalize())

            changes.append(f"**{attr}:** `{old}` **→** `{new}`")

            # Set the embed icon and colour depending on which attribute changed.
            if any(name in attr for name in ("Channel", "deaf", "mute")):
                if new is None or new is True:
                    # Left a channel or was muted/deafened.
                    icon = Icons.voice_state_red
                    colour = Colours.soft_red
                elif old is None or old is True:
                    # Joined a channel or was unmuted/undeafened.
                    icon = Icons.voice_state_green
                    colour = Colours.soft_green

        if not changes:
            return

        member_str = escape_markdown(str(member))
        message = "\n".join(f"{Emojis.bullet} {item}"
                            for item in sorted(changes))
        message = f"**{member_str}** (`{member.id}`)\n{message}"

        await self.send_log_message(
            icon_url=icon,
            colour=colour,
            title="Voice state updated",
            text=message,
            thumbnail=member.avatar_url_as(static_format="png"),
            channel_id=Channels.voice_log)
Esempio n. 17
0
    async def on_message_edit(self, before: discord.Message,
                              after: discord.Message) -> None:
        """Log message edit event to message change log."""
        if (not before.guild or before.guild.id != GuildConstant.id
                or before.channel.id in GuildConstant.ignored
                or before.author.bot):
            return

        self._cached_edits.append(before.id)

        if before.content == after.content:
            return

        author = before.author
        channel = before.channel

        if channel.category:
            before_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{before.clean_content}")

            after_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{after.clean_content}")
        else:
            before_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{before.clean_content}")

            after_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{before.id}`\n"
                "\n"
                f"{after.clean_content}")

        if before.edited_at:
            # Message was previously edited, to assist with self-bot detection, use the edited_at
            # datetime as the baseline and create a human-readable delta between this edit event
            # and the last time the message was edited
            timestamp = before.edited_at
            delta = humanize_delta(
                relativedelta(after.edited_at, before.edited_at))
            footer = f"Last edited {delta} ago"
        else:
            # Message was not previously edited, use the created_at datetime as the baseline, no
            # delta calculation needed
            timestamp = before.created_at
            footer = None

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (Before)",
                                    before_response,
                                    channel_id=Channels.message_log,
                                    timestamp_override=timestamp,
                                    footer=footer)

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (After)",
                                    after_response,
                                    channel_id=Channels.message_log,
                                    timestamp_override=after.edited_at)
Esempio n. 18
0
    async def on_member_update(self, before: discord.Member,
                               after: discord.Member) -> None:
        """Log member update event to user log."""
        if before.guild.id != GuildConstant.id:
            return

        if before.id in self._ignored[Event.member_update]:
            self._ignored[Event.member_update].remove(before.id)
            return

        diff = DeepDiff(before, after)
        changes = []
        done = []

        diff_values = {}

        diff_values.update(diff.get("values_changed", {}))
        diff_values.update(diff.get("type_changes", {}))
        diff_values.update(diff.get("iterable_item_removed", {}))
        diff_values.update(diff.get("iterable_item_added", {}))

        diff_user = DeepDiff(before._user, after._user)

        diff_values.update(diff_user.get("values_changed", {}))
        diff_values.update(diff_user.get("type_changes", {}))
        diff_values.update(diff_user.get("iterable_item_removed", {}))
        diff_values.update(diff_user.get("iterable_item_added", {}))

        for key, value in diff_values.items():
            if not key:  # Not sure why, but it happens
                continue

            key = key[5:]  # Remove "root." prefix

            if "[" in key:
                key = key.split("[", 1)[0]

            if "." in key:
                key = key.split(".", 1)[0]

            if key in done or key in MEMBER_CHANGES_SUPPRESSED:
                continue

            if key == "_roles":
                new_roles = after.roles
                old_roles = before.roles

                for role in old_roles:
                    if role not in new_roles:
                        changes.append(
                            f"**Role removed:** {role.name} (`{role.id}`)")

                for role in new_roles:
                    if role not in old_roles:
                        changes.append(
                            f"**Role added:** {role.name} (`{role.id}`)")

            else:
                new = value.get("new_value")
                old = value.get("old_value")

                if new and old:
                    changes.append(
                        f"**{key.title()}:** `{old}` **->** `{new}`")

            done.append(key)

        if before.name != after.name:
            changes.append(
                f"**Username:** `{before.name}` **->** `{after.name}`")

        if before.discriminator != after.discriminator:
            changes.append(
                f"**Discriminator:** `{before.discriminator}` **->** `{after.discriminator}`"
            )

        if before.display_name != after.display_name:
            changes.append(
                f"**Display name:** `{before.display_name}` **->** `{after.display_name}`"
            )

        if not changes:
            return

        message = ""

        for item in sorted(changes):
            message += f"{Emojis.bullet} {item}\n"

        message = f"**{after}** (`{after.id}`)\n{message}"

        await self.send_log_message(
            Icons.user_update,
            Colour.blurple(),
            "Member updated",
            message,
            thumbnail=after.avatar_url_as(static_format="png"),
            channel_id=Channels.userlog)
Esempio n. 19
0
    async def on_raw_message_edit(
            self, event: discord.RawMessageUpdateEvent) -> None:
        """Log raw message edit event to message change log."""
        try:
            channel = self.bot.get_channel(int(event.data["channel_id"]))
            message = await channel.fetch_message(event.message_id)
        except discord.NotFound:  # Was deleted before we got the event
            return

        if (not message.guild or message.guild.id != GuildConstant.id
                or message.channel.id in GuildConstant.ignored
                or message.author.bot):
            return

        await asyncio.sleep(1)  # Wait here in case the normal event was fired

        if event.message_id in self._cached_edits:
            # It was in the cache and the normal event was fired, so we can just ignore it
            self._cached_edits.remove(event.message_id)
            return

        author = message.author
        channel = message.channel

        if channel.category:
            before_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{message.id}`\n"
                "\n"
                "This message was not cached, so the message content cannot be displayed."
            )

            after_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{message.id}`\n"
                "\n"
                f"{message.clean_content}")
        else:
            before_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{message.id}`\n"
                "\n"
                "This message was not cached, so the message content cannot be displayed."
            )

            after_response = (
                f"**Author:** {author} (`{author.id}`)\n"
                f"**Channel:** #{channel.name} (`{channel.id}`)\n"
                f"**Message ID:** `{message.id}`\n"
                "\n"
                f"{message.clean_content}")

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (Before)",
                                    before_response,
                                    channel_id=Channels.message_log)

        await self.send_log_message(Icons.message_edit,
                                    Colour.blurple(),
                                    "Message edited (After)",
                                    after_response,
                                    channel_id=Channels.message_log)
Esempio n. 20
0
    async def superstarify(self, ctx: Context, member: Member, duration: str, *, forced_nick: str = None):
        """
        This command will force a random superstar name (like Taylor Swift) to be the user's
        nickname for a specified duration. If a forced_nick is provided, it will use that instead.

        :param ctx: Discord message context
        :param ta:
        If provided, this function shows data for that specific tag.
        If not provided, this function shows the caller a list of all tags.
        """

        log.debug(
            f"Attempting to superstarify {member.display_name} for {duration}. "
            f"forced_nick is set to {forced_nick}."
        )

        embed = Embed()
        embed.colour = Colour.blurple()

        params = {
            "user_id": str(member.id),
            "duration": duration
        }

        if forced_nick:
            params["forced_nick"] = forced_nick

        response = await self.bot.http_session.post(
            URLs.site_superstarify_api,
            headers=self.headers,
            json=params
        )

        response = await response.json()

        if "error_message" in response:
            log.warning(
                "Encountered the following error when trying to superstarify the user:\n"
                f"{response.get('error_message')}"
            )
            embed.colour = Colour.red()
            embed.title = random.choice(NEGATIVE_REPLIES)
            embed.description = response.get("error_message")
            return await ctx.send(embed=embed)

        else:
            forced_nick = response.get('forced_nick')
            end_time = response.get("end_timestamp")
            image_url = response.get("image_url")

            embed.title = "Congratulations!"
            embed.description = (
                f"Your previous nickname, **{member.display_name}**, was so bad that we have decided to change it. "
                f"Your new nickname will be **{forced_nick}**.\n\n"
                f"You will be unable to change your nickname until \n**{end_time}**.\n\n"
                "If you're confused by this, please read our "
                f"[official nickname policy]({NICKNAME_POLICY_URL})."
            )
            embed.set_image(url=image_url)

            # Log to the mod_log channel
            log.trace("Logging to the #mod-log channel. This could fail because of channel permissions.")
            mod_log = self.bot.get_channel(Channels.modlog)
            await mod_log.send(
                f":middle_finger: {member.name}#{member.discriminator} (`{member.id}`) "
                f"has been superstarified by **{ctx.author.name}**. Their new nickname is `{forced_nick}`. "
                f"They will not be able to change their nickname again until **{end_time}**"
            )

            await self.moderation.notify_infraction(
                user=member,
                infr_type="Superstarify",
                duration=duration,
                reason=f"Your nickname didn't comply with our [nickname policy]({NICKNAME_POLICY_URL})."
            )

            # Change the nick and return the embed
            log.debug("Changing the users nickname and sending the embed.")
            await member.edit(nick=forced_nick)
            await ctx.send(embed=embed)
Esempio n. 21
0
    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter=None):
        """
        Get a list of all tags or a specified tag.

        :param ctx: Discord message context
        :param tag_name:
        If provided, this function shows data for that specific tag.
        If not provided, this function shows the caller a list of all tags.
        """

        def _command_on_cooldown(tag_name) -> bool:
            """
            Check if the command is currently on cooldown.
            The cooldown duration is set in constants.py.

            This works on a per-tag, per-channel basis.
            :param tag_name: The name of the command to check.
            :return: True if the command is cooling down. Otherwise False.
            """

            now = time.time()

            cooldown_conditions = (
                tag_name
                and tag_name in self.tag_cooldowns
                and (now - self.tag_cooldowns[tag_name]["time"]) < Cooldowns.tags
                and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id
            )

            if cooldown_conditions:
                return True
            return False

        if _command_on_cooldown(tag_name):
            time_left = Cooldowns.tags - (time.time() - self.tag_cooldowns[tag_name]["time"])
            log.warning(f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
                        f"Cooldown ends in {time_left:.1f} seconds.")
            return

        tags = []

        embed = Embed()
        embed.colour = Colour.red()
        tag_data = await self.get_tag_data(tag_name)

        # If we found something, prepare that data
        if tag_data:
            embed.colour = Colour.blurple()

            if tag_name:
                log.debug(f"{ctx.author} requested the tag '{tag_name}'")
                embed.title = tag_name

                if ctx.channel.id not in TEST_CHANNELS:
                    self.tag_cooldowns[tag_name] = {
                        "time": time.time(),
                        "channel": ctx.channel.id
                    }

            else:
                embed.title = "**Current tags**"

            if isinstance(tag_data, list):
                log.debug(f"{ctx.author} requested a list of all tags")
                tags = [f"**»**   {tag['tag_name']}" for tag in tag_data]
                tags = sorted(tags)

            else:
                embed.description = tag_data['tag_content']
                if tag_data['image_url'] is not None:
                    embed.set_image(url=tag_data['image_url'])

        # If not, prepare an error message.
        else:
            embed.colour = Colour.red()

            if isinstance(tag_data, dict):
                log.warning(f"{ctx.author} requested the tag '{tag_name}', but it could not be found.")
                embed.description = f"**{tag_name}** is an unknown tag name. Please check the spelling and try again."
            else:
                log.warning(f"{ctx.author} requested a list of all tags, but the tags database was empty!")
                embed.description = "**There are no tags in the database!**"

            if tag_name:
                embed.set_footer(text="To show a list of all tags, use !tags.")
                embed.title = "Tag not found."

        # Paginate if this is a list of all tags
        if tags:
            log.debug(f"Returning a paginated list of all tags.")
            return await LinePaginator.paginate(
                (lines for lines in tags),
                ctx, embed,
                footer_text="To show a tag, type !tags <tagname>.",
                empty=False,
                max_lines=15
            )

        return await ctx.send(embed=embed)
Esempio n. 22
0
    async def server_info(self, ctx: Context):
        """
        Returns an embed full of
        server information.
        """

        created = time_since(ctx.guild.created_at, precision="days")
        features = ", ".join(ctx.guild.features)
        region = ctx.guild.region

        # How many of each type of channel?
        roles = len(ctx.guild.roles)
        channels = ctx.guild.channels
        text_channels = 0
        category_channels = 0
        voice_channels = 0
        for channel in channels:
            if type(channel) == TextChannel:
                text_channels += 1
            elif type(channel) == CategoryChannel:
                category_channels += 1
            elif type(channel) == VoiceChannel:
                voice_channels += 1

        # How many of each user status?
        member_count = ctx.guild.member_count
        members = ctx.guild.members
        online = 0
        dnd = 0
        idle = 0
        offline = 0
        for member in members:
            if str(member.status) == "online":
                online += 1
            elif str(member.status) == "offline":
                offline += 1
            elif str(member.status) == "idle":
                idle += 1
            elif str(member.status) == "dnd":
                dnd += 1

        embed = Embed(colour=Colour.blurple(),
                      description=textwrap.dedent(f"""
                **Server information**
                Created: {created}
                Voice region: {region}
                Features: {features}

                **Counts**
                Members: {member_count:,}
                Roles: {roles}
                Text: {text_channels}
                Voice: {voice_channels}
                Channel categories: {category_channels}

                **Members**
                {Emojis.status_online} {online}
                {Emojis.status_idle} {idle}
                {Emojis.status_dnd} {dnd}
                {Emojis.status_offline} {offline}
            """))

        embed.set_thumbnail(url=ctx.guild.icon_url)

        await ctx.send(embed=embed)
Esempio n. 23
0
    async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:
        """
        Show the Zen of Python.

        Without any arguments, the full Zen will be produced.
        If an integer is provided, the line with that index will be produced.
        If a string is provided, the line which matches best will be produced.
        """
        embed = Embed(
            colour=Colour.blurple(),
            title="The Zen of Python",
            description=ZEN_OF_PYTHON
        )

        if search_value is None:
            embed.title += ", by Tim Peters"
            await ctx.send(embed=embed)
            return

        zen_lines = ZEN_OF_PYTHON.splitlines()

        # handle if it's an index int
        if isinstance(search_value, int):
            upper_bound = len(zen_lines) - 1
            lower_bound = -1 * upper_bound
            if not (lower_bound <= search_value <= upper_bound):
                raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.")

            embed.title += f" (line {search_value % len(zen_lines)}):"
            embed.description = zen_lines[search_value]
            await ctx.send(embed=embed)
            return

        # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead
        # exact word.
        for i, line in enumerate(zen_lines):
            for word in line.split():
                if word.lower() == search_value.lower():
                    embed.title += f" (line {i}):"
                    embed.description = line
                    await ctx.send(embed=embed)
                    return

        # handle if it's a search string and not exact word
        matcher = difflib.SequenceMatcher(None, search_value.lower())

        best_match = ""
        match_index = 0
        best_ratio = 0

        for index, line in enumerate(zen_lines):
            matcher.set_seq2(line.lower())

            # the match ratio needs to be adjusted because, naturally,
            # longer lines will have worse ratios than shorter lines when
            # fuzzy searching for keywords. this seems to work okay.
            adjusted_ratio = (len(line) - 5) ** 0.5 * matcher.ratio()

            if adjusted_ratio > best_ratio:
                best_ratio = adjusted_ratio
                best_match = line
                match_index = index

        if not best_match:
            raise BadArgument("I didn't get a match! Please try again with a different search term.")

        embed.title += f" (line {match_index}):"
        embed.description = best_match
        await ctx.send(embed=embed)
Esempio n. 24
0
    async def member_info(self, ctx: Context, *, user: discord.Member) -> None:
        """Returns info about a member."""
        roles = ""
        activities = ""
        joined = user.joined_at
        name = user.name
        gstatus = user.status
        wstatus = user.web_status
        dstatus = user.desktop_status
        mstatus = user.mobile_status
        bot = user.bot
        nick = user.nick
        boostsince = user.premium_since
        pc = user.is_on_pc()
        web = user.is_on_web()
        mobile = user.is_on_mobile()
        top = user.top_role
        
        #List of users roles
        for i in range(len(user.roles)):
            roles += str(user.roles[i].mention) + ", "
                
        embed = discord.Embed(title=str(user.display_name) + "'s Information", colour=Colour.blurple())
        embed.add_field(name="Member joined on", value=joined.strftime("%A %d %B %Y %H:%M"))
        embed.add_field(name="Members Nickname", value=nick)
        if boostsince == None:
            embed.add_field(name="Boosted server since", value="Never boosted")
        else:
            embed.add_field(name="Boosted server since", value=boostsince.strftime("%A %d %B %Y %H:%M"))
        embed.add_field(name=":robot: Bot?", value=bot)
        embed.add_field(name="On PC?", value=pc)
        embed.add_field(name="On Web?", value=web)
        embed.add_field(name=":iphone: On Mobile?", value=mobile)
        embed.add_field(name=":computer: Desktop App", value=dstatus)
        embed.add_field(name="Web/Browser App", value=wstatus)
        embed.add_field(name=":iphone: Android/iOS App", value=mstatus)
        embed.add_field(name="Roles Member is in", value=roles)
        embed.add_field(name="Highest role of Member", value=top)
        embed.set_thumbnail(url=user.avatar_url)

        await ctx.send(embed=embed)
Esempio n. 25
0
class Information(Cog):
    """A cog with commands for generating embeds with server info, such as server stats and user info."""
    def __init__(self, bot: Bot):
        self.bot = bot

    @staticmethod
    def role_can_read(channel: GuildChannel, role: Role) -> bool:
        """Return True if `role` can read messages in `channel`."""
        overwrites = channel.overwrites_for(role)
        return overwrites.read_messages is True

    def get_staff_channel_count(self, guild: Guild) -> int:
        """
        Get the number of channels that are staff-only.

        We need to know two things about a channel:
        - Does the @everyone role have explicit read deny permissions?
        - Do staff roles have explicit read allow permissions?

        If the answer to both of these questions is yes, it's a staff channel.
        """
        channel_ids = set()
        for channel in guild.channels:
            if channel.type is ChannelType.category:
                continue

            everyone_can_read = self.role_can_read(channel, guild.default_role)

            for role in constants.STAFF_ROLES:
                role_can_read = self.role_can_read(channel,
                                                   guild.get_role(role))
                if role_can_read and not everyone_can_read:
                    channel_ids.add(channel.id)
                    break

        return len(channel_ids)

    @staticmethod
    def get_channel_type_counts(guild: Guild) -> str:
        """Return the total amounts of the various types of channels in `guild`."""
        channel_counter = Counter(c.type for c in guild.channels)
        channel_type_list = []
        for channel, count in channel_counter.items():
            channel_type = str(channel).title()
            channel_type_list.append(f"{channel_type} channels: {count}")

        channel_type_list = sorted(channel_type_list)
        return "\n".join(channel_type_list)

    @with_role(*constants.MODERATION_ROLES)
    @command(name="roles")
    async def roles_info(self, ctx: Context) -> None:
        """Returns a list of all roles and their corresponding IDs."""
        # Sort the roles alphabetically and remove the @everyone role
        roles = sorted(ctx.guild.roles[1:], key=lambda role: role.name)

        # Build a list
        role_list = []
        for role in roles:
            role_list.append(f"`{role.id}` - {role.mention}")

        # Build an embed
        embed = Embed(
            title=
            f"Role information (Total {len(roles)} role{'s' * (len(role_list) > 1)})",
            colour=Colour.blurple())

        await LinePaginator.paginate(role_list, ctx, embed, empty=False)

    @with_role(*constants.MODERATION_ROLES)
    @command(name="role")
    async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None:
        """
        Return information on a role or list of roles.

        To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks.
        """
        parsed_roles = []
        failed_roles = []

        for role_name in roles:
            if isinstance(role_name, Role):
                # Role conversion has already succeeded
                parsed_roles.append(role_name)
                continue

            role = utils.find(lambda r: r.name.lower() == role_name.lower(),
                              ctx.guild.roles)

            if not role:
                failed_roles.append(role_name)
                continue

            parsed_roles.append(role)

        if failed_roles:
            await ctx.send(
                f":x: Could not retrieve the following roles: {', '.join(failed_roles)}"
            )

        for role in parsed_roles:
            h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb())

            embed = Embed(
                title=f"{role.name} info",
                colour=role.colour,
            )
            embed.add_field(name="ID", value=role.id, inline=True)
            embed.add_field(name="Colour (RGB)",
                            value=f"#{role.colour.value:0>6x}",
                            inline=True)
            embed.add_field(name="Colour (HSV)",
                            value=f"{h:.2f} {s:.2f} {v}",
                            inline=True)
            embed.add_field(name="Member count",
                            value=len(role.members),
                            inline=True)
            embed.add_field(name="Position", value=role.position)
            embed.add_field(name="Permission code",
                            value=role.permissions.value,
                            inline=True)

            await ctx.send(embed=embed)

    @command(name="server", aliases=["server_info", "guild", "guild_info"])
    async def server_info(self, ctx: Context) -> None:
        """Returns an embed full of server information."""
        created = time_since(ctx.guild.created_at, precision="days")
        features = ", ".join(ctx.guild.features)
        region = ctx.guild.region

        roles = len(ctx.guild.roles)
        member_count = ctx.guild.member_count
        channel_counts = self.get_channel_type_counts(ctx.guild)

        # How many of each user status?
        statuses = Counter(member.status for member in ctx.guild.members)
        embed = Embed(colour=Colour.blurple())

        # How many staff members and staff channels do we have?
        staff_member_count = len(
            ctx.guild.get_role(constants.Roles.helpers).members)
        staff_channel_count = self.get_staff_channel_count(ctx.guild)

        # Because channel_counts lacks leading whitespace, it breaks the dedent if it's inserted directly by the
        # f-string. While this is correctly formated by Discord, it makes unit testing difficult. To keep the formatting
        # without joining a tuple of strings we can use a Template string to insert the already-formatted channel_counts
        # after the dedent is made.
        embed.description = Template(
            textwrap.dedent(f"""
                **Server information**
                Created: {created}
                Voice region: {region}
                Features: {features}

                **Channel counts**
                $channel_counts
                Staff channels: {staff_channel_count}

                **Member counts**
                Members: {member_count:,}
                Staff members: {staff_member_count}
                Roles: {roles}

                **Member statuses**
                {constants.Emojis.status_online} {statuses[Status.online]:,}
                {constants.Emojis.status_idle} {statuses[Status.idle]:,}
                {constants.Emojis.status_dnd} {statuses[Status.dnd]:,}
                {constants.Emojis.status_offline} {statuses[Status.offline]:,}
            """)).substitute({"channel_counts": channel_counts})
        embed.set_thumbnail(url=ctx.guild.icon_url)

        await ctx.send(embed=embed)

    @command(name="user", aliases=["user_info", "member", "member_info"])
    async def user_info(self, ctx: Context, user: Member = None) -> None:
        """Returns info about a user."""
        if user is None:
            user = ctx.author

        # Do a role check if this is being executed on someone other than the caller
        elif user != ctx.author and not with_role_check(
                ctx, *constants.MODERATION_ROLES):
            await ctx.send(
                "You may not use this command on users other than yourself.")
            return

        # Non-staff may only do this in #bot-commands
        if not with_role_check(ctx, *constants.STAFF_ROLES):
            if not ctx.channel.id == constants.Channels.bot_commands:
                raise InWhitelistCheckFailure(constants.Channels.bot_commands)

        embed = await self.create_user_embed(ctx, user)

        await ctx.send(embed=embed)

    async def create_user_embed(self, ctx: Context, user: Member) -> Embed:
        """Creates an embed containing information on the `user`."""
        created = time_since(user.created_at, max_units=3)

        # Custom status
        custom_status = ''
        for activity in user.activities:
            if isinstance(activity, CustomActivity):
                state = ""

                if activity.name:
                    state = escape_markdown(activity.name)

                emoji = ""
                if activity.emoji:
                    # If an emoji is unicode use the emoji, else write the emote like :abc:
                    if not activity.emoji.id:
                        emoji += activity.emoji.name + " "
                    else:
                        emoji += f"`:{activity.emoji.name}:` "

                custom_status = f'Status: {emoji}{state}\n'

        name = str(user)
        if user.nick:
            name = f"{user.nick} ({name})"

        badges = []

        for badge, is_set in user.public_flags:
            if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}",
                                            None)):
                badges.append(emoji)

        joined = time_since(user.joined_at, max_units=3)
        roles = ", ".join(role.mention for role in user.roles[1:])

        desktop_status = STATUS_EMOTES.get(user.desktop_status,
                                           constants.Emojis.status_online)
        web_status = STATUS_EMOTES.get(user.web_status,
                                       constants.Emojis.status_online)
        mobile_status = STATUS_EMOTES.get(user.mobile_status,
                                          constants.Emojis.status_online)

        fields = [("User information",
                   textwrap.dedent(f"""
                    Created: {created}
                    Profile: {user.mention}
                    ID: {user.id}
                    {custom_status}
                """).strip()),
                  ("Member information",
                   textwrap.dedent(f"""
                    Joined: {joined}
                    Roles: {roles or None}
                """).strip()),
                  ("Status",
                   textwrap.dedent(f"""
                    {desktop_status} Desktop
                    {web_status} Web
                    {mobile_status} Mobile
                """).strip())]

        # Show more verbose output in moderation channels for infractions and nominations
        if ctx.channel.id in constants.MODERATION_CHANNELS:
            fields.append(await self.expanded_user_infraction_counts(user))
            fields.append(await self.user_nomination_counts(user))
        else:
            fields.append(await self.basic_user_infraction_counts(user))

        # Let's build the embed now
        embed = Embed(title=name, description=" ".join(badges))

        for field_name, field_content in fields:
            embed.add_field(name=field_name, value=field_content, inline=False)

        embed.set_thumbnail(url=user.avatar_url_as(static_format="png"))
        embed.colour = user.top_role.colour if roles else Colour.blurple()

        return embed