예제 #1
0
class Nyoom(commands.Cog):

    def __init__(self, client):
        self.client = client
        self.client_extras = ClientTools(client)
        self.database_extras = DatabaseTools(self.client_extras)

    async def get_times(self, user_id=None):
        """
        username : user you want to get messages for

        Returns:

        times: list of all timestamps of users messages
        """
        if user_id is None:
            get_time = "SELECT `time` FROM `messages_detailed` ORDER BY TIME ASC"
            cursor.execute(get_time)
        else:
            get_time = "SELECT `time` FROM `messages_detailed` WHERE `user_id` = %s ORDER BY TIME ASC"
            cursor.execute(get_time, (user_id,))
        timesA = cursor.fetchall()
        times = []
        for time in timesA:
            times.append(time[0])
        return times

    async def calculate_nyoom(self, output, user_id=None):
        # load interval between messages we're using from the configs
        interval = config['discord']['nyoom_interval']
        times = await self.get_times(user_id=user_id)
        # group them into periods of activity
        periods = []
        curPeriod = [times[0], times[0], 0]  # begining of period, end of period, number of messages in period
        for time in times:
            if time > curPeriod[1] + datetime.timedelta(0,
                                                        interval):  # if theres more than a 10min dif between this time and last time
                # make a new period
                periods.append(curPeriod)
                curPeriod = [time, time, 1]
            else:
                curPeriod[1] = time  # the period now ends with the most recent timestamp
                curPeriod[2] += 1  # add the message to the period
        # sum the total length of activity periods and divide by total number of messages
        totalT = 0
        totalM = len(times)

        for period in periods:
            totalT += ((period[1] - period[
                0]).total_seconds() / 60) + 1  # total number of minutes for the activity period, plus a fudge factor to prevent single message periods from causing a divide by zero issue later
        totalT /= 60  # makes the total active time and nyoom_metric count hours of activity rather than minutes
        nyoom_metric = totalM / totalT  # number of message per minute during periods of activity
        return totalM, totalT, nyoom_metric

    @commands.command()
    async def nyoom(self, ctx, user: discord.Member = None):
        """
        Calculate the specified user's nyoom metric.
        e.g. The number of messages per hour they post while active (posts within 10mins of each other count as active)

        user : user to get nyoom metric for, if not author
        """
        if user is None:
            user = ctx.message.author

        output = await ctx.send(strings['nyoom_calc']['status']['calculating'] + strings['emojis']['loading'])
        username = self.database_extras.opted_in(user_id=user.id)
        if not username:
            return await output.edit(content=output.content + '\n' + strings['nyoom_calc']['status']['not_opted_in'])
        # grab a list of times that user has posted
        totalM, totalT, nyoom_metric = await self.calculate_nyoom(output, user_id=user.id)
        await output.delete()
        embed = discord.Embed(title="Nyoom metrics", color=colours.blue)
        embed.add_field(name="Message count", value=totalM)
        embed.add_field(name="Total hours", value=totalT)
        embed.add_field(name="Nyoom metric", value=nyoom_metric)
        embed.set_footer(text="These values may not be 100% accurate")
        return await ctx.send(embed=embed)

    @commands.command(aliases=["nyoomserver", "ns", "n_s"])
    async def nyoom_server(self, ctx):
        """
        Calculate nyoom metric for entire server
        """
        output = await ctx.send(strings['nyoom_calc']['status']['calculating'] + strings['emojis']['loading'])
        totalM, totalT, nyoom_metric = await self.calculate_nyoom(output)

        # prepare the final embed to send
        embed = discord.Embed(title="Nyoom metrics", color=colours.blue)
        embed.add_field(name="Message count", value=totalM)
        embed.add_field(name="Total hours", value=totalT)
        embed.add_field(name="Nyoom metric", value=nyoom_metric)
        embed.set_footer(text="These values may not be 100% accurate")
        await output.delete()
        return await ctx.send(embed=embed)
예제 #2
0
class Tagger(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.client_extras = ClientTools(client)
        self.database_extras = DatabaseTools(self.client_extras)

    @commands.command(aliases=["t"])
    async def tagger(self,
                     ctx,
                     nsfw: bool = False,
                     selected_channel: discord.TextChannel = None):
        """
        Generates tags for you based on what you talk about
        """
        if (not ctx.message.channel.is_nsfw()) and nsfw:
            return await ctx.send(strings['tagger']['errors']['nsfw'].format(
                str(ctx.author)))

        output = await ctx.send(strings['tagger']['title'] +
                                strings['emojis']['loading'])

        await output.edit(content=output.content + "\n" +
                          strings['tagger']['status']['messages'])
        async with ctx.channel.typing():
            username = self.database_extras.opted_in(user_id=ctx.author.id)
            if not username:
                return await output.edit(
                    content=output.content +
                    strings['tagger']['errors']['not_opted_in'])
            messages, channels = await self.database_extras.get_messages(
                ctx.author.id, config['limit'])

            text = []

            text = await self.client_extras.build_messages(
                ctx,
                nsfw,
                messages,
                channels,
                selected_channel=selected_channel)

            text1 = ""
            for m in text:
                text1 += str(m) + "\n"
            await output.edit(content=output.content +
                              strings['emojis']['success'] + "\n" +
                              strings['tagger']['status']['analytical_data'])
            algo = algo_client.algo('nlp/AutoTag/1.0.1')
            await output.delete()
            response = algo.pipe(text1)
            tags = list(response.result)
            tag_str = ""
            for tag in tags:
                tag_str = "- " + tag + "\n" + tag_str
            em = await self.client_extras.markov_embed(
                "Tags for " + str(ctx.author), tag_str)
            output = await ctx.send(embed=em)
        emoji = await self.client_extras.get_delete_emoji()
        emoji = emoji[1]
        return await self.client_extras.delete_option(self.client, output, ctx,
                                                      emoji)
예제 #3
0
class Admin(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.database_tools = DatabaseTools(client)
        self.client_tools = ClientTools(client)

    @commands.group(hidden=True)
    async def debug(self, ctx):
        """Debug utilities for AGSE and Discord"""
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "Invalid params. Run `help debug` to get all commands.")

    @is_server_allowed()
    @debug.command(aliases=["isprocessed", "processed"])
    async def is_processed(self, ctx, user=None):
        """
        Admin command used to check if a member has opted in
        """
        if user is None:
            user = ctx.author.name

        msg = await ctx.send(strings['process_check']['status']['checking'])
        if not self.database_tools.opted_in(user=user):
            return await msg.edit(
                content=strings['process_check']['status']['not_opted_in'])
        return await ctx.edit(
            content=strings['process_check']['status']['opted_in'])

    @is_owner_or_admin()
    @debug.command(aliases=["dumproles"])
    async def dump_roles(self, ctx):
        """
        Dump all roles to a text file on the host
        """
        to_write = ""
        for guild in self.client.guilds:
            to_write += "\n\n=== {} ===\n\n".format(str(guild))
            for role in guild.roles:
                to_write += "{} : {}\n".format(role.name, role.id)
        roles = open("roles.txt", "w")
        roles.write(to_write)
        roles.close()
        em = discord.Embed(title="Done", description="Check roles.txt")
        await ctx.channel.send(embed=em)

    @debug.command(aliases=["lag"])
    async def latency(self, ctx, detailed=None):
        detailed = bool(detailed)
        # this is a tuple, with [0] being the shard_id, and [1] being the latency
        latencies = self.client.latencies
        lowest_lag = latencies[0]
        highest_lag = latencies[0]
        sum = 0
        for i in latencies:
            if i[1] < lowest_lag[1]:
                lowest_lag = i
            if i[1] > highest_lag[1]:
                highest_lag = i
            # could probably do this in a one liner, but may as well as we have to iterate anyway
            sum += i[1]

        avg = (sum / len(latencies))

        embed = discord.Embed(title="Latency")

        # add specific information about latency
        embed.add_field(name="Avg", value="{}".format(str(avg)))
        embed.add_field(name="Lowest Latency",
                        value="{} on shard {}".format(lowest_lag[1],
                                                      lowest_lag[0]))
        embed.add_field(name="Highest Latency",
                        value="{} on shard {}".format(highest_lag[1],
                                                      highest_lag[0]))

        if detailed:
            embed.add_field(name="RawData", value=str(latencies))

        return await ctx.channel.send(embed=embed)

    @debug.command(aliases=["role_id"])
    async def roleid(self, ctx, role_name):
        for role in ctx.guild.roles:
            if role_name.lower() == role.name.lower():
                return await ctx.send(role.id)
        return await ctx.send(embed=discord.Embed(
            title="Could not find role {}".format(role_name)))

    @is_server_allowed()
    @commands.group(aliases=["rolem"])
    async def role_manage(self, ctx):
        """Manages AGSE roles (ping groups)"""
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "Invalid params. Run `help rolem` to get all commands.")

    @role_manage.command()
    async def add(self, ctx, *, role_name):
        """Add a role. Note: by default, it isn't joinable"""
        if role_name[0] == '"' and role_name[-1] == '"':
            role_name = role_name[1:-1]
        role_check = get_role(ctx.guild.id, role_name)
        em = discord.Embed(title="Success",
                           description="Created role {}".format(role_name),
                           color=green)
        if role_check is not None:
            em = discord.Embed(title="Error",
                               description="Role is already in the DB",
                               color=red)
        else:
            query = "INSERT INTO `gssp`.`roles` (`role_name`, `guild_id`) VALUES (%s, %s);"
            cursor.execute(query, (role_name, ctx.guild.id))
            cnx.commit()
        return await ctx.channel.send(embed=em)

    @role_manage.command(aliases=["remove"])
    async def delete(self, ctx, *, role_name):
        """Deletes a role - cannot be undone!"""
        if role_name[0] == '"' and role_name[-1] == '"':
            role_name = role_name[1:-1]
        role_check = get_role(ctx.guild.id, role_name)
        em = discord.Embed(title="Success",
                           description="Deleted role {}".format(role_name),
                           color=green)
        if role_check is None:
            em = discord.Embed(
                title="Error",
                description="{} is not in the DB".format(role_name),
                color=red)
        else:
            query = "DELETE FROM `gssp`.`roles` WHERE `role_name` = %s AND `guild_id` = %s"
            cursor.execute(query, (role_name, ctx.guild.id))
            cnx.commit()
        return await ctx.channel.send(embed=em)

    @role_manage.command(aliases=["togglepingable"])
    async def pingable(self, ctx, *, role_name):
        """Change a role from not pingable to pingable or vice versa"""
        if role_name[0] == '"' and role_name[-1] == '"':
            role_name = role_name[1:-1]
        role = get_role(ctx.guild.id, role_name)
        if role is None:
            return await ctx.channel.send(
                embed=discord.Embed(title='Error',
                                    description='Could not find that role',
                                    color=red))
        if role['is_pingable'] == 1:
            update_query = "UPDATE `gssp`.`roles` SET `is_pingable`='0' WHERE `role_id`=%s AND `guild_id` = %s;"
            text = "not pingable"
        else:
            update_query = "UPDATE `gssp`.`roles` SET `is_pingable`='1' WHERE `role_id`=%s AND `guild_id` = %s;"
            text = "pingable"
        cursor.execute(update_query, (
            role['role_id'],
            ctx.guild.id,
        ))
        cnx.commit()
        await ctx.channel.send(
            embed=discord.Embed(title="SUCCESS",
                                description="Set {} ({}) to {}".format(
                                    role['role_name'], role['role_id'], text),
                                color=green))

    @role_manage.command(
        aliases=["togglejoinable", "togglejoin", "toggle_join"])
    async def joinable(self, ctx, *, role_name):
        """
        Toggles whether a role is joinable
        """
        if role_name[0] == '"' and role_name[-1] == '"':
            role_name = role_name[1:-1]
        role = get_role(ctx.guild.id, role_name)
        if role is None:
            em = discord.Embed(
                title="Error",
                description="Could not find role {}".format(role_name),
                color=red)
            return await ctx.channel.send(embed=em)
        if role['is_joinable'] == 1:
            update_query = "UPDATE `gssp`.`roles` SET `is_joinable`='0' WHERE `role_id`=%s;"
            text = "not joinable"
        else:
            update_query = "UPDATE `gssp`.`roles` SET `is_joinable`='1' WHERE `role_id`=%s;"
            text = "joinable"
        cursor.execute(update_query, (role['role_id'], ))
        em = discord.Embed(title="Success",
                           description="Set {} ({} to {}".format(
                               role['role_name'], role['role_id'], text),
                           color=green)
        cnx.commit()

        await ctx.channel.send(embed=em)

    @is_owner_or_admin()
    @commands.group(aliases=["config"])
    async def settings(self, ctx):
        """Manages settings of AGSE"""
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "Invalid params. Run `help settings` to get all commands.")

    @settings.command(aliases=[
        "resyncroles", "syncroles", "rolesync", "role_sync", "sync_roles"
    ])
    async def resync_roles(self, ctx):
        """
        Force refresh the roles in the database with the roles discord has.
        """
        for guild in self.client.guilds:
            for role in guild.roles:
                if role.name != "@everyone":
                    try:
                        cursor.execute(insert_role, (role.id, role.name))
                    except mysql.connector.errors.IntegrityError:
                        pass

                    # this is designed to assist with migration, by moving old discord role members over to the new
                    # system seamlessly
                    member_ids = []
                    for member in role.members:
                        member_ids.append(member.id)
                    role_db = DbRole(role.id, role.name, 0, members=member_ids)
                    role_db.save_members()
                    cursor.execute(update_role,
                                   (emoji.demojize(role.name), role.id))
        await ctx.send(embed=discord.Embed(
            title="Success", description="Resynced roles.", color=green))

    @is_owner_or_admin()
    @settings.group(aliases=["permissions"])
    async def perms(self, ctx):
        """Manages AGSE roles (ping groups)"""
        if ctx.invoked_subcommand is None:
            await ctx.send(
                "Run `help settings perms` to get info on subcommands")

    @perms.command()
    async def promote_role(self, ctx, role_id):
        """
        Add a role to the list of allowed roles
        """
        role = ctx.guild.get_role(int(role_id))

        if role is None:
            return await ctx.send(
                embed=discord.Embed(title="Error",
                                    description="That role does not exist",
                                    color=red))
        settings = guild_settings.get_settings(guild=ctx.guild)
        if role_id in settings['staff_roles']:
            return await ctx.send(
                embed=discord.Embed(title="Error",
                                    description="Role already has admin perms",
                                    color=red))
        settings['staff_roles'].append(role_id)
        guild_settings.write_settings(settings)
        return await ctx.send(embed=discord.Embed(
            title="Success",
            description="Role {} added to admin list".format(role.name),
            color=green))

    @perms.command()
    async def demote_role(self, ctx, role_id):
        role_id = int(role_id)
        role_to_remove = ctx.guild.get_role(int(role_id))
        if role_to_remove is None:
            return await ctx.send(
                embed=discord.Embed(title="Error",
                                    description="That role does not exist",
                                    color=red))
        settings = guild_settings.get_settings(guild=ctx.guild)
        if role_id in ctx.author.roles:  # this means the user is removing a role that gives them perms
            users_permitted_roles = [
            ]  # list of roles that give user permission to run this
            for role in ctx.author.roles:
                for role_existing in settings['staff_roles']:
                    if role_existing == role.id:
                        users_permitted_roles.append(role)
            if len(users_permitted_roles) <= 1:
                return await ctx.send(embed=discord.Embed(
                    title="Error",
                    description=
                    "You cannot remove a role that gives permissions without another role which has permissions to do so",
                    color=red))
        try:
            settings['staff_roles'].remove(str(role_id))
            guild_settings.write_settings(settings)
            return await ctx.send(embed=discord.Embed(
                title="Success",
                description="Removed {} from permitted role list".format(
                    role_to_remove.name),
                color=green))
        except ValueError:
            return await ctx.send(embed=discord.Embed(
                title="Error",
                description=
                "That role does not exist in the permitted role list",
                color=red))

    @is_owner_or_admin()
    @commands.command()
    async def sync(self, ctx):
        clone_target = self.client.get_guild(
            config['discord'].get("clone_server_target"))

        def generate_progress_embed(m_text, colour=yellow, url=None):
            em = discord.Embed(
                title="Server Clone Progress",
                description="Status: {text}".format(text=m_text),
                colour=colour)
            if url is not None:
                em.add_field(name="Invite link", value=url)
            return em

        guild = ctx.guild
        # we just need to now create an instant invite to *somewhere* on the server
        progress = await ctx.send(embed=generate_progress_embed(
            "Dumping existing data from {guild.name}".format(guild=guild)))
        channels = []
        roles = []

        def get_channel_position(old_id=None, new_id=None):
            if new_id is None and old_id is None:
                raise AttributeError
            for x in range(0, len(channels)):
                channel = channels[x]
                # the and is not None prevent us from returning whatever channel has None as an attribute
                if (channel.get("old_id") == old_id and old_id
                        is not None) or (channel.get("new_id") == new_id
                                         and new_id is not None):
                    return x
            return None

        def get_channel(old_id=None, new_id=None):
            position = get_channel_position(old_id=old_id, new_id=new_id)
            if position is None:
                return None
            return channels[position]

        def add_channel(old_channel, new_channel=None):
            to_append = (dict(old_id=old_channel.id, old_channel=old_channel))
            if new_channel is None:
                to_append['new_id'] = None
                to_append['new_channel'] = None
            else:
                to_append['new_id'] = new_channel.id
                to_append['new_channel'] = new_channel
            channels.append(to_append)

        def set_new_channel(old_channel_id, new_channel):
            # we don't use the new_channel id, as not merged yet
            position = get_channel_position(old_id=old_channel_id)
            new_channel = get_channel_object_dict(new_channel)
            channels[position]['new_channel'] = new_channel
            channels[position]['new_id'] = new_channel['id']

        def get_role_position(old_id=None, new_id=None):
            if new_id is None and old_id is None:
                return None  # means we don't have to do unnecesary searches
            if old_id is not None:
                old_id = int(old_id)
            if new_id is not None:
                new_id = int(new_id)
            for x in range(0, len(roles)):
                role = roles[x]
                # the and is not None prevent us from returning whatever channel has None as an attribute
                if (role.get("old_id") == old_id and old_id is not None) or (
                        role.get("new_id") == new_id and new_id is not None):
                    return x
            return None

        def get_role(old_id=None, new_id=None):
            position = get_role_position(old_id=old_id, new_id=new_id)
            if position is None:
                return None
            return roles[position]

        def add_role(old_role, new_role=None):
            to_append = (dict(old_id=old_role.id, old_role=old_role))
            if new_role is None:
                to_append['new_id'] = None
                to_append['new_role'] = None
            else:
                to_append['new_id'] = new_role.id
                to_append['new_role'] = new_role
            roles.append(to_append)

        def set_new_role(old_role_id, new_role):
            # we don't use the new_role id, as not merged yet
            position = get_role_position(old_id=old_role_id)
            roles[position]['new_role'] = new_role
            roles[position]['new_id'] = new_role.id

        def get_role_object_dict(role):
            if type(role) == dict:
                return role  # if already role, just return it
            return dict(id=role.id,
                        name=role.name,
                        permissions=role.permissions.value,
                        colour=role.colour.value,
                        hoist=role.hoist,
                        mentionable=role.mentionable)

        def get_role_dicts(roles=roles):
            backup_roles = roles
            role_dicts = []
            for role in roles:
                if role.get('old_role') is not None:
                    role['old_role'] = get_role_object_dict(role['old_role'])
                if role.get('new_role') is not None:
                    role['new_role'] = get_role_object_dict(role['new_role'])
                role_dicts.append(role)
            return role_dicts

        def get_channel_type(channel):

            if type(channel) == discord.channel.TextChannel:
                return "Text"
            if type(channel) == discord.channel.VoiceChannel:
                return "Voice"
            if type(channel) == discord.channel.CategoryChannel:
                return "Category"
            return "Unknown"

        def get_channel_object_dict(channel):
            if type(channel) == dict:
                return channel  # already converted
            new_overwrites = []
            overwrites = channel.overwrites
            for overwrite in overwrites:
                allow, deny = overwrite[1].pair()
                if type(overwrite[0]) == discord.role.Role:
                    role = get_role(old_id=overwrite[0].id)
                    if role is None:
                        to_append = dict(
                            grantee=dict(old_id=overwrite[0].id, type="Role"))
                    else:
                        to_append = dict(grantee=dict(
                            old_id=role.get('old_id'),
                            new_id=role.get('new_id'),
                            old_name=role.get('old_role', dict()).get('name'),
                            type="Role"))
                else:  # user overwrite
                    to_append = dict(
                        grantee=dict(id=overwrite[0].id, type="User"))
                to_append['allow_permission'] = allow.value
                to_append['deny_permission'] = deny.value
                new_overwrites.append(to_append)
            to_return = dict(id=channel.id,
                             name=channel.name,
                             type=get_channel_type(channel),
                             overwrites=new_overwrites,
                             position=channel.position)
            if to_return['type'] != "Category":
                if channel.category is not None:
                    to_return['category'] = get_channel_object_dict(
                        channel.category)
                else:
                    to_return['category'] = None
                if to_return['type'] == "Text":
                    # do text
                    to_return['topic'] = channel.topic
                    to_return['slowmode_delay'] = channel.slowmode_delay
                    to_return['nsfw'] = channel.nsfw

                elif to_return['type'] == "Voice":
                    # do voice
                    to_return['bitrate'] = channel.bitrate
                    to_return['user_limit'] = channel.user_limit
            return to_return

        def get_channel_dicts(channels=channels):
            backup_channels = channels
            channel_dicts = []
            for channel in channels:
                if channel.get('old_channel') is not None:
                    channel['old_channel'] = get_channel_object_dict(
                        channel['old_channel'])
                if channel.get('new_channel') is not None:
                    channel['new_role'] = get_channel_object_dict(
                        channel['new_channel'])
                channel_dicts.append(channel)
            channels = backup_channels
            return channel_dicts

        def generate_overwrites(old_channel):
            overwrites = dict()
            for overwrite in old_channel['overwrites']:
                allow = discord.Permissions(overwrite['allow_permission'])
                deny = discord.Permissions(overwrite['deny_permission'])
                permission_pair = discord.PermissionOverwrite.from_pair(
                    allow, deny)
                target = None  # we do this incase down the road there's a new type of grantee we haven't handled
                # if a user isn't in the new server, we can't add overwrites for them
                if overwrite['grantee']['type'] == "User":
                    target = clone_target.get_member(
                        overwrite['grantee']['id'])
                # this is the code which will convert the old_id in the overwrite into the new_id
                elif overwrite['grantee']['type'] == "Role":
                    role = get_role(old_id=overwrite['grantee']['old_id'])
                    if role is not None:
                        target = clone_target.get_role(role['new_id'])
                    else:
                        old_role = ctx.guild.get_role(
                            overwrite['grantee']['old_id'])
                        if old_role.name == "@everyone":
                            target = clone_target.default_role
                        else:
                            print(
                                "Could not find new role pair for old role with ID {}"
                                .format(overwrite['grantee']['old_id']))
                if target is not None:
                    overwrites[target] = permission_pair
            return overwrites

        s_channels = guild.channels
        s_channels.sort(key=lambda x: x.position, reverse=False)

        for channel in s_channels:
            add_channel(channel)

        s_roles = guild.roles
        s_roles.reverse()
        for role in s_roles:
            if role.name != "@everyone":
                add_role(role)
        await progress.edit(embed=generate_progress_embed(
            "Wiping roles of {clone_target.name}".format(
                clone_target=clone_target)))

        for role in clone_target.roles:
            try:
                await role.delete(reason="Cleaning for copying")
            except discord.errors.HTTPException:
                pass

        await progress.edit(embed=generate_progress_embed(
            "Wiping channels of {clone_target.name}".format(
                clone_target=clone_target)))
        for channel in clone_target.channels:
            await channel.delete(reason="Cleaning for copying")
        print("Wiped channels")

        await progress.edit(embed=generate_progress_embed(
            "Creating new roles in {clone_target.name}".format(
                clone_target=clone_target)))
        for role in get_role_dicts():
            old_role = role['old_role']
            logger.info("Adding role{id} - {name}".format(
                id=role['old_id'], name=role['old_role']['name']))
            new_role_to_merge = await clone_target.create_role(
                name=old_role['name'],
                permissions=discord.Permissions(
                    permissions=old_role['permissions']),
                colour=discord.Colour(old_role['colour']),
                hoist=old_role['hoist'],
                mentionable=old_role['mentionable'])
            set_new_role(old_role['id'], new_role_to_merge)

        get_role_dicts(
        )  # this converts all the new channels into nice dictionary formats

        await progress.edit(embed=generate_progress_embed(
            "Creating new channels in {clone_target.name}".format(
                clone_target=clone_target)))

        # first, we add the categories
        channels_to_add = get_channel_dicts()
        channels.sort(key=lambda x: x['old_channel']['position'],
                      reverse=False)
        for channel in channels_to_add:
            old_channel = channel['old_channel']
            if old_channel['type'] == "Category":
                logger.info("Adding category {id} - {name}".format(
                    id=old_channel['id'], name=old_channel['name']))
                # build overwrites
                overwrites = generate_overwrites(old_channel)

                channel = await clone_target.create_category_channel(
                    old_channel['name'],
                    overwrites=overwrites,
                    reason="Syncing of channels")
                set_new_channel(old_channel['id'], channel)
        # this makes sure everything is still in dictionary formats
        channels_to_add = get_channel_dicts()
        # now we know all our categories are added, we can add all other channels
        for channel in channels_to_add:
            old_channel = channel['old_channel']
            if old_channel['type'] != "Category":
                logger.info("Adding {type} channel {id} - {name}".format(
                    type=old_channel['type'],
                    id=old_channel['id'],
                    name=old_channel['name']))
                overwrites = generate_overwrites(old_channel)

                category = get_channel(
                    old_id=old_channel['category'].get('id'))
                if category is not None:
                    category_id = category['new_id']
                    # gets the role object that we need to create the channel
                    category = clone_target.get_channel(category_id)
                if old_channel['type'] == "Text":
                    channel = await clone_target.create_text_channel(
                        old_channel['name'],
                        overwrites=overwrites,
                        reason="Syncing of channels",
                        position=old_channel['position'],
                        topic=old_channel['topic'],
                        slowmode_delay=old_channel['slowmode_delay'],
                        nsfw=old_channel['nsfw'],
                        category=category)
                elif old_channel['type'] == "Voice":
                    channel = await clone_target.create_voice_channel(
                        old_channel['name'],
                        overwrites=overwrites,
                        reason="Syncing of channels",
                        position=old_channel['position'],
                        bitrate=old_channel['bitrate'],
                        user_limit=old_channel['user_limit'],
                        category=category)
                channel = get_channel_object_dict(channel)
                set_new_channel(old_channel['id'], channel)
        with open(".last_sync.json", "w") as f:
            f.write(
                json.dumps(dict(roles=get_role_dicts(roles=roles),
                                channels=get_channel_dicts(channels=channels)),
                           indent=4))

        invite = await clone_target.text_channels[0].create_invite(
            max_age=0,
            max_uses=0,
            unique=True,
            reason="Generating invite link to join with")
        await progress.edit(embed=generate_progress_embed(
            "Done!", colour=green, url=invite.url))
예제 #4
0
class Sentiment(commands.Cog):

    def __init__(self, client):
        self.client = client
        self.client_extras = ClientTools(client)
        self.database_extras = DatabaseTools(self.client_extras)

    @commands.command(aliases=["s"])
    async def sentiment(self, ctx, raw: bool = False, nsfw: bool = False, selected_channel: discord.TextChannel = None):
        """
        Calculate sentiment.json from your messages!
        """
        raw = bool(raw)
        if (not ctx.message.channel.is_nsfw()) and nsfw:
            return await ctx.send(strings['tagger']['errors']['nsfw'].format(str(ctx.author)))

        output = await ctx.send(strings['tagger']['title'] + strings['emojis']['loading'])

        await output.edit(content=output.content + "\n" + strings['tagger']['status']['messages'])
        async with ctx.channel.typing():
            username = self.database_extras.opted_in(user_id=ctx.author.id)
            if not username:
                return await output.edit(content=output.content + strings['tagger']['errors']['not_opted_in'])
            messages, channels = await self.database_extras.get_messages(ctx.author.id, config['limit'] / 10)

            text = []

            text = await self.client_extras.build_messages(ctx, nsfw, messages, channels,
                                                           selected_channel=selected_channel)
            await output.edit(
                content=output.content + strings['emojis']['success'] + "\n" + strings['tagger']['status'][
                    'analytical_data'])
            algo = algo_client.algo('nlp/SocialSentimentAnalysis/0.1.4')
            response = algo.pipe({"sentenceList": text})
            tags = list(response.result)
            await output.delete()
            file = open("sentiment.json", "w")
            file.write(str(tags))
            positive = 0
            negative = 0
            neutral = 0
            compound = 0
            for tag in tags:
                positive += tag['positive']
                negative += tag['negative']
                neutral += tag['neutral']
                compound += tag['compound']

            em = discord.Embed(title="Sentiment", color=colours.blue)
            positive = (math.ceil((positive / len(tags)) * 10000)) / 100
            negative = (math.ceil((negative / len(tags)) * 10000)) / 100
            neutral = (math.ceil((neutral / len(tags)) * 10000)) / 100
            compound = (math.ceil((compound / len(tags)) * 10000)) / 100
            if raw:

                file.close()

                em.add_field(name="Positivity", value=str(positive))
                em.add_field(name="Negativity", value=str(negative))
                em.add_field(name="Neutrality", value=str(neutral), inline=False)
                em.add_field(name = "Flip-floppity-ness", value = str(compound))
                em.add_field(name="Info", value="*Max value for these are 100 points, min are -100 points*",
                             inline=False)
            else:

                positive = math.floor(positive / 2)
                negative = math.floor(negative / 2)
                em.add_field(name="Niceness", value=(":heart:" * positive), inline=False)
                em.add_field(name="Evilness", value=(":smiling_imp:" * math.floor(negative)), inline=False)
                shoe = random.randint(0, 5)
                if shoe == 0:
                    emoji_flip = ":sandal:"
                elif shoe == 1:
                    emoji_flip = ":arrows_counterclockwise:"
                elif shoe == 2:
                    emoji_flip = ":yin_yang:"
                elif shoe == 3:
                    emoji_flip = ":libra:"
                else:
                    emoji_flip = "<a:zthonkspin:399368569500205056>"
                em.add_field(name = "Flip-floppitiness", value = (emoji_flip * math.floor(compound)), inline = False)

            output = await ctx.send(embed=em)
        emoji = await self.client_extras.get_delete_emoji()
        emoji = emoji[1]
        return await self.client_extras.delete_option(self.client, output, ctx, emoji)
class ClientTools():
    def __init__(self, client):
        self.database_tools = DatabaseTools(client)
        self.client = client

    def channel_allowed(self, channel_id, existing_channel, nsfw=False):
        """
        Check if a channel is allowed in current context

        channel_id: ID of channel
        existing_channel: channel object of existing channel
        nsfw: whether to only return NSFW channels
        """
        channel = self.client.get_channel(int(channel_id))
        if channel is None:
            return False

        enabled = False
        for group in enabled_groups:
            if str(channel.category).lower() == str(group).lower():
                enabled = True
                break

        if not enabled:
            return False

        if not existing_channel.is_nsfw() and bool(nsfw):
            return False

        if channel.is_nsfw():
            # checks if user wants / is allowed explicit markovs
            return bool(nsfw)
            # this means that if the channel *is* NSFW, we return True, but if it isn't, we return False
        else:  # channel is SFW
            if bool(nsfw):
                return False  # this stops SFW chats from being included in NSFW markovs

        return True

    async def build_messages(self,
                             ctx,
                             nsfw,
                             messages,
                             channels,
                             selected_channel=None):
        """
            Returns/appends to a list messages from a user
            Params:
            messages: list of messages
            channel: list of channels for messages
            selected_channel: Not required, but channel to filter to. If none, filtering is disabled.
            text = list of text that already exists. If not set, we just create one
        """
        text = []

        for counter, m in enumerate(messages):

            if self.channel_allowed(channels[counter], ctx.message.channel,
                                    nsfw):
                if selected_channel is not None:
                    if self.client.get_channel(int(
                            channels[counter])).id == selected_channel.id:
                        text.append(m)
                else:
                    text.append(m)
        return text

    async def get_delete_emoji(self):
        delete_emoji = self.client.get_emoji(int(strings['emojis']['delete']))
        if delete_emoji is not None:
            emoji_name = delete_emoji.name
        else:
            emoji_name = "❌"
        return emoji_name, delete_emoji

    async def error_embed(self,
                          ctx,
                          error,
                          message=None,
                          colour=discord.Embed.Empty):
        embed = discord.Embed(title='Command Error', colour=colour)
        embed.description = str(error)
        embed.add_field(name='Server', value=ctx.guild)
        embed.add_field(name='Channel', value=ctx.channel.mention)
        embed.add_field(name='User', value=ctx.author)
        embed.add_field(name='Message', value=ctx.message.content)
        embed.timestamp = datetime.datetime.utcnow()
        await ctx.send(content=message, embed=embed)

    async def markov_embed(self, title, message):
        em = discord.Embed(title=title, description=message)
        name = await self.get_delete_emoji()
        name = name[0]
        em.set_footer(text=strings['markov']['output']['footer'].format(name))
        return em

    async def delete_option(self,
                            client,
                            message,
                            ctx,
                            delete_emoji,
                            timeout=config['discord']['delete_timeout']):
        """Utility function that allows for you to add a delete option to the end of a command.
        This makes it easier for users to control the output of commands, esp handy for random output ones."""
        await message.add_reaction(delete_emoji)

        def check(r, u):
            return str(
                r) == str(delete_emoji
                          ) and u == ctx.author and r.message.id == message.id

        try:
            await client.wait_for("reaction_add", timeout=timeout, check=check)
            await message.remove_reaction(delete_emoji, client.user)
            await message.remove_reaction(delete_emoji, ctx.author)
            em = discord.Embed(title=str(ctx.message.author) +
                               " deleted message",
                               description="User deleted this message.")

            return await message.edit(embed=em)
        except concurrent.futures._base.TimeoutError:
            await message.remove_reaction(delete_emoji, client.user)

    async def build_data_profile(self, members, limit=50000):
        """
        Used for building a data profile based on a user

        Members: list of members we want to import for
        Guild: Guild object
        Limit: limit of messages to be imported
        """
        for guild in self.client.guilds:
            for cur_channel in guild.text_channels:
                adding = False
                for group in enabled_groups:
                    if str(cur_channel.category).lower() == str(group).lower():
                        adding = True
                        break

                if adding:
                    logger.info("Logging from {}".format(cur_channel.name))
                    counter = 0
                    already_added = 0
                    async for message in cur_channel.history(limit=limit,
                                                             reverse=True):
                        if message.author in members:
                            self.database_tools.add_message_to_db(message)
                    logger.info(
                        "{} scraped for {} users - added {} messages, found {} already added"
                        .format(cur_channel.name, len(members), counter,
                                already_added))

    async def process_message(self, message):
        await self.check_flags(message)
        user_exp = self.database_tools.opted_in(user_id=message.author.id)

        if user_exp is not False:
            self.database_tools.add_message_to_db(message)
        logger.debug("Message from {}".format(user_exp))
        # this records analytical data - don't adjust this without reading
        # Discord TOS first
        try:
            cursor.execute(add_message,
                           (int(message.id), str(message.channel.id),
                            message.created_at.strftime('%Y-%m-%d %H:%M:%S')))
            cnx.commit()
        except mysql.connector.errors.IntegrityError:
            pass
        try:
            # if its double(or more) prefixed then it cant be a command (?word is a command, ????? is not)
            if message.content[len(config['discord']
                                   ['prefix'])] == config['discord']['prefix']:
                return
        except IndexError:
            return

    async def check_flags(self, message):
        if type(message.channel) == discord.DMChannel:
            return
        if message.author.id == self.client.user.id:
            return
        matches = []
        flag_settings = guild_settings.get_bad_words(message.guild)
        flags = flag_settings['words']
        regexes = flag_settings.get('regex')
        if regexes is None:
            regexes = []
        channel_id = flag_settings['alert_channel']
        if channel_id is None:
            return
        for flag in flags:
            if flag.lower() != "" and flag in message.content.lower():
                matches.append(flag)
        for regex in regexes:
            try:
                temp_regex = re.compile(regex, re.IGNORECASE)
                for word in message.content.lower().split(" "):
                    if temp_regex.match(word):
                        matches.append(word)
            except:
                pass  # we do this because some regex may be bad

        if len(matches) > 0:
            embed = discord.Embed(title="Potential usage of flag detected",
                                  color=colours.dark_red)

            embed.add_field(name="Flag(s)", value=str(matches))
            embed.add_field(name="Message", value=str(message.content))
            embed.add_field(name="Author", value=str(message.author.mention))
            embed.add_field(name="Author ID", value=str(message.author.id))
            embed.add_field(name="Channel", value=str(message.channel.mention))
            embed.add_field(name="Time", value=str(message.created_at))
            embed.add_field(name="Message ID", value=str(message.id))
            embed.add_field(name="Guild ID",
                            value=str(message.channel.guild.id))
            embed.add_field(name="Guild", value=str(message.channel.guild))

            embed.add_field(
                name="Message Link",
                value=str("https://discordapp.com/channels/{}/{}/{}".format(
                    message.guild.id, message.channel.id, message.id)),
                inline=False)

            channel = self.client.get_channel(channel_id)
            await channel.send(embed=embed)

    async def optout_user(self, user):
        """
        Opt a user out of experiments, and delete their data
        Returns number of messages deleted
        """
        logger.info("Deleting data for user ID {}".format(user.id))
        cursor.execute("DELETE FROM users WHERE user_id = %s", (user.id, ))
        result = cursor.execute(
            "DELETE FROM messages_detailed WHERE user_id = %s", (user.id, ))
        cnx.commit()
        logger.info("Data deleted.")
예제 #6
0
class Markov(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.database_tools = DatabaseTools(client)
        self.client_tools = ClientTools(client)

    @commands.command(aliases=["m_s"])
    async def markov_server(self,
                            ctx,
                            nsfw: bool = False,
                            selected_channel: discord.TextChannel = None):
        """
        Generates markov output based on entire server's messages.
        """
        nsfw_mismatch = False
        if selected_channel is not None:
            if selected_channel.is_nsfw() and not nsfw:
                nsfw_mismatch = True
            elif not selected_channel.is_nsfw() and nsfw:
                nsfw_mismatch = True
        if nsfw_mismatch:
            return await ctx.send(embed=discord.Embed(
                title="Error",
                description=
                "The selected channel and the NSFW flag do not match. Please ensure these are both correct.",
                color=colours.red))
        output = await ctx.send(strings['markov']['title'] +
                                strings['emojis']['loading'])
        await output.edit(content=output.content + "\n" +
                          strings['markov']['status']['messages'])
        async with ctx.channel.typing():
            text = []
            messages, channels = await self.database_tools.get_messages(
                ctx.author.id, config['limit_server'], server=True)
            text = await self.client_tools.build_messages(
                ctx,
                nsfw,
                messages,
                channels,
                selected_channel=selected_channel)

            text1 = ""
            for m in text:
                text1 += str(m) + "\n"
            if len(text) < 10:
                return await output.edit(
                    content=output.content +
                    strings['markov']['errors']['low_activity'])
            try:
                await output.edit(
                    content=output.content + strings['emojis']['success'] +
                    "\n" + strings['markov']['status']['building_markov'])
                # text_model = POSifiedText(text)
                text_model = markovify.NewlineText(
                    text, state_size=config['state_size'])
            except KeyError:
                return ctx.send('Not enough data yet, sorry!')
            await output.edit(content=output.content +
                              strings['emojis']['success'] + "\n" +
                              strings['markov']['status']['making'])
            text = text_model.make_short_sentence(140)
            attempt = 0
            while (True):
                attempt += 1
                if attempt >= 10:
                    return await ctx.send(
                        strings['markov']['errors']['failed_to_generate'])
                message_formatted = str(text)
                if message_formatted != "None":
                    break

            await output.delete()
            em = await self.client_tools.markov_embed(
                strings['markov']['output']['title_server'], message_formatted)
            output = await ctx.send(embed=em)
        return await self.client_tools.delete_option(
            self.client, output, ctx,
            self.client.get_emoji(int(strings['emojis']['delete'])) or "❌")

    @commands.command(aliases=["m"])
    async def markov(self,
                     ctx,
                     nsfw: bool = False,
                     selected_channel: discord.TextChannel = None):
        """
        Generates markov output for user who ran this command
        """
        if (not ctx.message.channel.is_nsfw()) and nsfw:
            return await ctx.send(strings['markov']['errors']['nsfw'].format(
                str(ctx.author)))

        output = await ctx.send(strings['markov']['title'] +
                                strings['emojis']['loading'])

        await output.edit(content=output.content + "\n" +
                          strings['markov']['status']['messages'])
        async with ctx.channel.typing():
            username = self.database_tools.opted_in(user_id=ctx.author.id)
            if not username:
                return await output.edit(
                    content=output.content +
                    strings['markov']['errors']['not_opted_in'])
            messages, channels = await self.database_tools.get_messages(
                ctx.author.id, config['limit'])

            text = []

            text = await self.client_tools.build_messages(
                ctx,
                nsfw,
                messages,
                channels,
                selected_channel=selected_channel)

            text1 = ""
            for m in text:
                text1 += str(m) + "\n"

            try:
                await output.edit(
                    content=output.content + strings['emojis']['success'] +
                    "\n" + strings['markov']['status']['building_markov'])
                # text_model = POSifiedText(text)
                text_model = markovify.NewlineText(
                    text, state_size=config['state_size'])
            except KeyError:
                return ctx.send('Not enough data yet, sorry!')

            attempt = 0
            while (True):
                attempt += 1
                if attempt >= 10:
                    await output.delete()
                    return await ctx.send(
                        strings['markov']['errors']['failed_to_generate'])
                new_sentance = text_model.make_short_sentence(140)
                message_formatted = str(new_sentance)
                if message_formatted != "None":
                    break

            await output.edit(content=output.content +
                              strings['emojis']['success'] + "\n" +
                              strings['markov']['status']['analytical_data'])
            await self.database_tools.save_markov(text_model, ctx.author.id)

            await output.edit(content=output.content +
                              strings['emojis']['success'] + "\n" +
                              strings['markov']['status']['making'])
            await output.delete()

            em = await self.client_tools.markov_embed(str(ctx.author),
                                                      message_formatted)
            output = await ctx.send(embed=em)
        return await self.client_tools.delete_option(
            self.client, output, ctx,
            self.client.get_emoji(int(strings['emojis']['delete'])) or "❌")
class Controls(commands.Cog):
    def __init__(self, client):
        self.client = client
        self.database_tools = DatabaseTools(client)
        self.client_tools = ClientTools(client)

    @commands.command(aliases=["optin", "opt_in"])
    async def experiments(self, ctx):
        """
        Opt into data analysis and experiments.
        """
        message = ctx.message
        channel = message.channel

        author = message.author
        create_user = "******"
        try:
            cursor.execute(create_user, (author.id, author.name))
            cnx.commit()

            em = discord.Embed(
                title=strings['data_collection']['opt_in_title'], description=opt_in_message)

            em.set_footer(text=strings['data_collection']['opt_in_footer'])
            return await channel.send(embed=em)
        except mysql.connector.errors.IntegrityError:
            get_user = "******"
            cursor.execute(get_user, (author.id,))

            opt_in_user = "******"

            cursor.execute(opt_in_user, (author.id,))
        await channel.send(strings['data_collection']['data_track_start'] + " for " + str(ctx.message.author))

        await self.client_tools.build_data_profile([author])
        await channel.send(strings['data_collection']['complete'].format(author.name))

    @commands.command(aliases=["opt_in_automated", "optinautomated"])
    async def automated(self, ctx):
        """
        Opt in to automated messages. Run this again to opt out.
        """
        if not self.database_tools.opted_in(user_id=ctx.author.id):
            return await ctx.channel.send(embed=discord.Embed(title="Error", description=strings['tagger']['errors']['not_opted_in'], color=colours.red))

        if self.database_tools.is_automated(ctx.author):
            output = await ctx.channel.send("Opting you out of automation.")
            query = "UPDATE `users` SET `automate_opted_in`=b'0' WHERE `user_id`=%s;"
            cursor.execute(query, (ctx.author.id,))
            cnx.commit()
            await output.delete()
            return await ctx.channel.send(embed=discord.Embed(title="Success", description='You will be removed from the pool on the next refresh (IE: when the bot goes back around in a loop again)'))
        else:
            output = await ctx.channel.send("Opting you into automation")
            query = "UPDATE`users` SET `automate_opted_in`=b'1' WHERE `user_id`=%s;"
            cursor.execute(query, (ctx.author.id,))
            cnx.commit()
            await output.delete()
            return await ctx.channel.send(embed=discord.Embed(title="Success", description='Opted in!', color=colours.green))

    @commands.command(aliases=["blacklist", "block_list", "black_list"])
    async def blocklist(self, ctx, command=None, word=None):
        """
        Prevents words from being shown publicly through methods such as markov and markov_server.
        Note: they will still be logged, and this just prevents them being shown in chat.

        Command: option to use
        Word: Word to add or remove from blocklist
        """
        pm_channel = (discord.channel.DMChannel == type(ctx.channel))
        if not pm_channel:
            try:
                await ctx.message.delete()
            except discord.errors.Forbidden:
                logger.warn(
                    "Could not delete blacklist command, lacking permissions")
        if command is None:
            return await ctx.send("""
        No subcommand selected - please enter a subcommand for your blocklist.

        ?blocklist add [word] : Add word to blocklist
        ?blocklist remove [word] : Remove word from blocklist
        ?blocklist get : Get PM of current blocklist
                """)
        # fetch current blocklist
        blockL = await self.database_tools.get_blocklist(ctx.author.id)
        update_blocklist = "UPDATE blocklists SET blocklist = %s WHERE user_id = %s"

        if command == "add":
            if word is None:
                return await ctx.send(strings['blocklist']['status']['no_word'],
                                      delete_after=config['discord']['delete_timeout'])
            msg = await ctx.send(strings['blocklist']['status']['adding'])

            # check if the word is already on the list. throw error if it is
            if word.lower() not in blockL:
                # if its not then add it
                blockL.append(word.lower())
                # update DB with new list
                new_json = json.dumps(blockL)
                cursor.execute(update_blocklist, (new_json, ctx.author.id,))

            else:
                await msg.delete()
                return await ctx.send(strings['blocklist']['status']['exist'])

        elif command == "remove":
            if word is None:
                return await ctx.send(strings['blocklist']['status']['no_word'],
                                      delete_after=config['discord']['delete_timeout'])
            msg = await ctx.send(strings['blocklist']['status']['removing'])

            # try and remove it from list (use a try statement, catching ValueError)
            try:
                blockL.remove(word.lower())
            except ValueError:
                return await msg.edit(content=strings['blocklist']['status']['not_exist'])

            # update DB with new list
            new_json = json.dumps(blockL)
            cursor.execute(update_blocklist, (new_json, ctx.author.id,))

        elif command == "get":
            # make it nice to look at
            if blockL == []:
                msg = strings['blocklist']['status']['empty']
            else:
                msg = strings['blocklist']['status']['list']
                for item in blockL:
                    # done so that the merge with the long string is only done once per word
                    part = ' ' + item + ','
                    msg += part
                msg = msg[:-1]  # trim off the trailing ,
            # send a private message with the nice to look at blocklist
            # this prevents the next commands from running
            return await ctx.author.send(msg)
        else:
            return await ctx.send("""
    No subcommand selected - please enter a subcommand for your blocklist.

    ?blocklist add [word] : Add word to blocklist
    ?blocklist remove [word] : Remove word from blocklist
    ?blocklist get : Get PM of current blocklist
                """)
        await msg.edit(content=strings['blocklist']['status']['complete'])

    @commands.command(aliases=["opt_out"])
    async def optout(self, ctx):
        """
        Run this to optout of experiments, and delete your data
        """
        em = discord.Embed(
            title=strings['data_collection']['opt_out_starting_title'], description=strings['data_collection']['opt_out_starting_message'], color=colours.red)
        current_embed = await ctx.send(embed=em)
        await self.client_tools.optout_user(ctx.author)
        em = discord.Embed(title=strings['data_collection']['opt_out_finish_title'],
                           description=strings['data_collection']['opt_out_finish_message'], color=colours.green)
        await current_embed.edit(embed=em)

    @commands.command(aliases=["datainfo", "data", "aboutme", "mydata", "my_data", "about_me"])
    async def data_info(self, ctx):
        """Returns the data we store on you."""
        async with ctx.channel.typing():
            username = self.database_tools.opted_in(user_id=ctx.author.id)
            if not username:
                em = discord.Embed(title="You are not opted in",
                                   description="As you are opted out, we have no info on you", color=colours.red)
                return await ctx.author.send(embed=em)
            message_count = await self.database_tools.get_message_count(
                user_id=ctx.author.id)
            blocklist = await self.database_tools.get_blocklist(ctx.author.id)
            blocklist_count = len(blocklist)
            # just to prevent this being used later as it contains sensitive info
            del(blocklist)
        em = discord.Embed(title="Data info", color=colours.blue)
        em.add_field(name="Message count", value=message_count)
        em.add_field(name="Blocklist count", value=blocklist_count)

        # we send to the author, so the data is kept private as per GDPR
        await ctx.author.send(embed=em)
        await ctx.message.delete()