Exemplo n.º 1
0
 def load(self, ctx):
     super(UtilitiesPlugin, self).load(ctx)
     self.reminder_task = Eventual(self.trigger_reminders)
     self.spawn_later(10, self.queue_reminders)
     self.server_owners = {
         '166304313004523520': "airplane, PokeBlobs", # OGNovuh#0003
         '175805198017626114': "Swagger's Hangout", # SwaggerSouls#4295
         '127060170802069505': "Hotel Fitz", # Fitz#9588
         '107941250228830208': "Jameskii", # Jameskii#0001
         '197316087879172096': "Daniel's Discord", # danielharrison#0001
         '226912511302041601': "xd Official Discord", # McCreamy#6793
         '265986744019714050': "Dooocord", # TheDooo#4877
         '202271067912404992': "Donut Operator's Discord", # DonutOperator#5512
     }
     self.server_managers = {
         '158200017906171904': "Hotel Fitz", # Xentiran#0007
         '191793155685744640': "Hotel Fitz, Jameskii, Swagger's Hangout", # Terminator966#0966
         '227888061353164810': "Hotel Fitz", # emily#2900
         '166304313004523520': "Jameskii", # OGNovuh#0003
         '324645959265157120': "Swagger's Hangout", # Mushy The Wizard#2319
         '285238487618420737': "Swagger's Hangout", # Boubie#0305
         '244907038667177984': "Swagger's Hangout", # Kata#7886
         '194903108495605760': "Swagger's Hangout", # Solaire#4156
         '138842652585099264': "Swagger's Hangout", # Xeano#4444
         '285712318464000000': "xd Official Discord", # Nuna#0001
         '340753167098839051': "Donut Operator's Discord", # Valkyrie#5555
         '339254723091890177': "Donut Operator's Discord", # AmazonPrimes#7409
     }
Exemplo n.º 2
0
    def test_eventual_accuracy(self):
        result = AsyncResult()
        should_be_called_at = None

        def f():
            result.set(datetime.utcnow())

        e = Eventual(f)
        should_be_called_at = datetime.utcnow() + timedelta(milliseconds=100)
        e.set_next_schedule(should_be_called_at)
        called_at = result.get()
        self.assertGreater(called_at, should_be_called_at)
Exemplo n.º 3
0
def test_eventual_coalesce():
    ref = {'value': 0}
    done = Event()
    e = Eventual(None)

    def func():
        ref['value'] += 1

        # Only the first time
        if ref['value'] == 1:
            e.set_next_schedule(datetime.utcnow() - timedelta(seconds=10))
            time.sleep(1)
            done.set()

    e.function = func
    e.set_next_schedule(datetime.utcnow() + timedelta(milliseconds=500))
    done.wait()
    assert ref['value'] == 1
Exemplo n.º 4
0
def test_eventual_lowering():
    result = AsyncResult()

    e = Eventual(lambda: result.set(datetime.utcnow()))
    e.set_next_schedule(datetime.utcnow() + timedelta(seconds=10))
    e.set_next_schedule(datetime.utcnow() + timedelta(seconds=5))

    should_be_called_at = datetime.utcnow() + timedelta(milliseconds=100)
    e.set_next_schedule(should_be_called_at)

    called_at = result.get()
    assert called_at >= should_be_called_at
    assert called_at <= should_be_called_at + timedelta(milliseconds=100)
Exemplo n.º 5
0
 def load(self, ctx):
     super(UtilitiesPlugin, self).load(ctx)
     self.reminder_task = Eventual(self.trigger_reminders)
     self.spawn_later(10, self.queue_reminders)
Exemplo n.º 6
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()
            ).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        event.msg.reply(random.choice(['heads', 'tails']))

    @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            return event.msg.reply(':warning: ending number too big!')

        if end <= start:
            return event.msg.reply(':warning: ending number must be larger than starting number!')

        event.msg.reply(str(random.randint(start, end)))

    @Plugin.command('cat', global_=True)
    def cat(self, event):
        # Sometimes random.cat gives us gifs (smh)
        for _ in range(3):
            try:
                r = requests.get('https://aws.random.cat/meow')
                r.raise_for_status()
            except:
                continue

            url = r.json()['file']
            if not url.endswith('.gif'):
                break
        else:
            return event.msg.reply('404 cat not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('cat.jpg', r.content)])

    @Plugin.command('urban', '<term:str...>', global_=True)
    def urban(self, event, term):
        r = requests.get('http://api.urbandictionary.com/v0/define', params={
            'term': term,
        })
        r.raise_for_status()
        data = r.json()

        if not len(data['list']):
            return event.msg.reply(':warning: no matches')

        event.msg.reply(u'{} - {}'.format(
            S(data['list'][0]['word']),
            S(data['list'][0]['definition']),
        ))

    @Plugin.command('pwnd', '<email:str>', global_=True)
    def pwnd(self, event, email):
        r = requests.get('https://haveibeenpwned.com/api/v2/breachedaccount/{}'.format(
            email
        ))

        if r.status_code == 404:
            return event.msg.reply(":white_check_mark: you haven't been pwnd yet, awesome!")

        r.raise_for_status()
        data = r.json()

        sites = []

        for idx, site in enumerate(data):
            sites.append(u'{} - {} ({})'.format(
                site['BreachDate'],
                site['Title'],
                site['Domain'],
            ))

        return event.msg.reply(u":warning: You've been pwnd on {} sites:\n{}".format(
            len(sites),
            '\n'.join(sites),
        ))

    @Plugin.command('geoip', '<ip:str>', global_=True)
    def geoip(self, event, ip):
        r = requests.get('http://json.geoiplookup.io/{}'.format(ip))
        r.raise_for_status()
        data = r.json()

        event.msg.reply(u'{} - {}, {} ({}) | {}, {}'.format(
            data['isp'],
            data['city'],
            data['region'],
            data['country_code'],
            data['latitude'],
            data['longitude'],
        ))

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            return event.msg.reply(u'Unknown emoji: `{}`'.format(S(emoji)))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(S(name)))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id))

        anim = emoji.startswith('<a:')
        fields.append('**Animated:** {}'.format('Yes' if anim else 'No'))

        ext = 'gif' if anim else 'png'
        url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields), attachments=[('emoji.'+ext, r.content)])

    # Full credit goes to: Xenthys
    def get_emoji_url(self, emoji):
        name = '-'.join(char.encode("unicode_escape").decode("utf-8")[2:].lstrip("0") for char in emoji)
        return 'https://cdn.oceanlord.me/emoji/{}.png'.format(name) if name.find('--') == -1 else None
    
    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        emojis = emojis.split(' ')
        if len(emojis) == 1:
            url = ext = ''
            emoji = emojis[0]
            if EMOJI_RE.match(emoji):
                _, eid = EMOJI_RE.findall(emoji)[0]
                ext = 'gif' if emoji.startswith('<a:') else 'png'
                url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext)
            else:
                ext = 'png'
                url = self.get_emoji_url(emoji)

            if not url:
                raise CommandFail('provided emoji is invalid')

            r = requests.get(url)
            try:
                r.raise_for_status()
            except requests.HTTPError:
                raise CommandFail('provided emoji is invalid')
            return event.msg.reply('', attachments=[('emoji.'+ext, r.content)])

        else:
            urls = []
            for emoji in emojis[:5]:
                if EMOJI_RE.match(emoji):
                    _, eid = EMOJI_RE.findall(emoji)[0]
                    urls.append('https://discordapp.com/api/emojis/{}.png'.format(eid))
                else:
                    url = self.get_emoji_url(emoji)
                    urls.append(url) if url else None

            width, height, images = 0, 0, []

            for r in Pool(6).imap(requests.get, urls):
                try:
                    r.raise_for_status()
                except requests.HTTPError:
                    continue

                img = Image.open(BytesIO(r.content))
                height = img.height if img.height > height else height
                width += img.width + 10
                images.append(img)

            if not images:
                raise CommandFail('provided emojis are invalid')

            image = Image.new('RGBA', (width, height))
            width_offset = 0
            for img in images:
                image.paste(img, (width_offset, 0))
                width_offset += img.width + 10

            combined = BytesIO()
            image.save(combined, 'png', quality=55)
            combined.seek(0)
            return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id
            ).order_by(Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            return event.msg.reply(u"I've never seen {}".format(user))

        event.msg.reply(u'I last saw {} {} ago (at {})'.format(
            user,
            humanize_duration(datetime.utcnow() - msg.timestamp),
            msg.timestamp
        ))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append((User.username ** u'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim.isdigit():
                queries.append((
                    (User.username == username) &
                    (User.discriminator == int(discrim))))

        users = User.select().where(reduce(operator.or_, queries))
        if len(users) == 0:
            return event.msg.reply(u'No users found for query `{}`'.format(S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        return event.msg.reply(u'Found the following users for your query: ```{}```'.format(
            u'\n'.join(map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25]))
        ))

    @Plugin.command('server', '[guild_id:snowflake]', global_=True)
    def server(self, event, guild_id=None):
        guild = self.state.guilds.get(guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('invalid server')

        content = []
        content.append(u'**\u276F Server Information**')

        created_at = to_datetime(guild.id)
        content.append(u'Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_at),
            created_at.isoformat(),
        ))
        content.append(u'Members: {}'.format(len(guild.members)))
        content.append(u'Features: {}'.format(', '.join(guild.features) or 'none'))

        content.append(u'\n**\u276F Counts**')
        text_count = sum(1 for c in guild.channels.values() if not c.is_voice)
        voice_count = len(guild.channels) - text_count
        content.append(u'Roles: {}'.format(len(guild.roles)))
        content.append(u'Text: {}'.format(text_count))
        content.append(u'Voice: {}'.format(voice_count))

        content.append(u'\n**\u276F Members**')
        status_counts = defaultdict(int)
        for member in guild.members.values():
            if not member.user.presence:
                status = Status.OFFLINE
            else:
                status = member.user.presence.status
            status_counts[status] += 1

        for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True):
            content.append(u'<{}> - {}'.format(
                STATUS_EMOJI[status], count
            ))

        embed = MessageEmbed()
        if guild.icon:
            embed.set_thumbnail(url=guild.icon_url)
            embed.color = get_dominant_colors_guild(guild)
        embed.description = '\n'.join(content)
        event.msg.reply('', embed=embed)

    @Plugin.command('info', '<user:user|snowflake>')
    def info(self, event, user):
        if isinstance(user, (int, long)):
            try:
                r = self.bot.client.api.http(Routes.USERS_GET, dict(user=user)) # hacky method cause this old version of Disco doesn't have a method for this and we're too lazy to update
                data = r.json()
                User = namedtuple('User', [
                    'avatar',
                    'discriminator',
                    'id',
                    'username',
                    'presence'
                ])
                user = User(
                    avatar=data["avatar"],
                    discriminator=data["discriminator"],
                    id=int(data["id"]),
                    username=data["username"],
                    presence=None
                )
            except APIException as e:
                raise CommandFail('invalid user')
        
        content = []
        content.append(u'**\u276F User Information**')
        content.append(u'ID: {}'.format(user.id))
        content.append(u'Profile: <@{}>'.format(user.id))

        if user.presence:
            emoji, status = get_status_emoji(user.presence)
            content.append('Status: {} <{}>'.format(status, emoji))
            if user.presence.game and user.presence.game.name:
                if user.presence.game.type == GameType.DEFAULT:
                    content.append(u'Game: {}'.format(user.presence.game.name))
                else:
                    content.append(u'Stream: [{}]({})'.format(user.presence.game.name, user.presence.game.url))

        created_dt = to_datetime(user.id)
        content.append('Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_dt),
            created_dt.isoformat()
        ))

        member = event.guild.get_member(user.id) if event.guild else None
        if member:
            content.append(u'\n**\u276F Member Information**')

            if member.nick:
                content.append(u'Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.isoformat(),
            ))

            if member.roles:
                content.append(u'Roles: {}'.format(
                    ', '.join((member.guild.roles.get(r).name for r in member.roles))
                ))

        # Execute a bunch of queries async
        newest_msg = Message.select(Message.timestamp).where(
            (Message.author_id == user.id) &
            (Message.guild_id == event.guild.id)
        ).limit(1).order_by(Message.timestamp.desc()).async()

        oldest_msg = Message.select(Message.timestamp).where(
            (Message.author_id == user.id) &
            (Message.guild_id == event.guild.id)
        ).limit(1).order_by(Message.timestamp.asc()).async()

        infractions = Infraction.select(
            Infraction.guild_id,
            fn.COUNT('*')
        ).where(
            (Infraction.user_id == user.id)
        ).group_by(Infraction.guild_id).tuples().async()

        voice = GuildVoiceSession.select(
            GuildVoiceSession.user_id,
            fn.COUNT('*'),
            fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at)
        ).where(
            (GuildVoiceSession.user_id == user.id) &
            (~(GuildVoiceSession.ended_at >> None))
        ).group_by(GuildVoiceSession.user_id).tuples().async()

        # Wait for them all to complete (we're still going to be as slow as the
        #  slowest query, so no need to be smart about this.)
        wait_many(newest_msg, oldest_msg, infractions, voice, timeout=10)
        tags = to_tags(guild_id=event.msg.guild.id)

        if newest_msg.value and oldest_msg.value:
            statsd.timing('sql.duration.newest_msg', newest_msg.value._query_time, tags=tags)
            statsd.timing('sql.duration.oldest_msg', oldest_msg.value._query_time, tags=tags)
            newest_msg = newest_msg.value.get()
            oldest_msg = oldest_msg.value.get()

            content.append(u'\n **\u276F Activity**')
            content.append('Last Message: {} ago ({})'.format(
                humanize_duration(datetime.utcnow() - newest_msg.timestamp),
                newest_msg.timestamp.isoformat(),
            ))
            content.append('First Message: {} ago ({})'.format(
                humanize_duration(datetime.utcnow() - oldest_msg.timestamp),
                oldest_msg.timestamp.isoformat(),
            ))

        if infractions.value:
            statsd.timing('sql.duration.infractions', infractions.value._query_time, tags=tags)
            infractions = list(infractions.value)
            total = sum(i[1] for i in infractions)
            content.append(u'\n**\u276F Infractions**')
            content.append('Total Infractions: {}'.format(total))
            content.append('Unique Servers: {}'.format(len(infractions)))

        if voice.value:
            statsd.timing('plugin.utilities.info.sql.voice', voice.value._query_time, tags=tags)
            voice = list(voice.value)
            content.append(u'\n**\u276F Voice**')
            content.append(u'Sessions: {}'.format(voice[0][1]))
            content.append(u'Time: {}'.format(humanize.naturaldelta(
                voice[0][2]
            )))

        embed = MessageEmbed()

        avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(
            user.id,
            user.avatar,
        )

        embed.set_author(name=u'{}#{}'.format(
            user.username,
            user.discriminator,
        ), icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        embed.color = get_dominant_colors_user(user, avatar)
        event.msg.reply('', embed=embed)

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))
        )

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder):
        message = reminder.message_id
        channel = self.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning('Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.format(
            message.author_id,
            reminder.created_at,
            humanize_duration(reminder.created_at - datetime.utcnow()),
            S(reminder.content)
        ))

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e: (
                    (e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and
                    e.user_id == message.author_id
                )
            ).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            msg.delete_reaction(SNOOZE_EMOJI)
            msg.delete_reaction(GREEN_TICK_EMOJI)

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.')
            return

        reminder.delete_instance()

    @Plugin.command('delete', '[reminder:str]', group='reminder', global_=True)
    @Plugin.command('remove', '[reminder:str]', group='reminder', global_=True)
    @Plugin.command('clean', '[reminder:str]', group='reminder', global_=True)
    @Plugin.command('clear', '[reminder:str]', group='reminder', global_=True)
    @Plugin.command('delete', '[reminder:str]', group='r', global_=True)
    @Plugin.command('remove', '[reminder:str]', group='r', global_=True)
    @Plugin.command('clean', '[reminder:str]', group='r', global_=True)
    @Plugin.command('clear', '[reminder:str]', group='r', global_=True)
    def cmd_remind_clear(self, event, reminder='all'):
        if reminder == 'all':
            count = Reminder.count_for_user(event.author.id)

            if Reminder.count_for_user(event.author.id) == 0:
                return event.msg.reply('<:{}> cannot clear reminders when you don\'t have any'.format(RED_TICK_EMOJI))
            
            msg = event.msg.reply('Ok, clear {} reminders?'.format(count))
            msg.chain(False).\
                add_reaction(GREEN_TICK_EMOJI).\
                add_reaction(RED_TICK_EMOJI)

            try:
                mra_event = self.wait_for_event(
                    'MessageReactionAdd',
                    message_id=msg.id,
                    conditional=lambda e: (
                        e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                        e.user_id == event.author.id
                    )).get(timeout=10)
            except gevent.Timeout:
                return
            finally:
                msg.delete()

            if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
                return
            
            count = Reminder.delete_all_for_user(event.author.id)
            return event.msg.reply(':ok_hand: I cleared {} reminders for you'.format(count))
        else:
            try:
                # stupid catch because python sucks
                try:
                    reminder = int(reminder)
                except:
                    return event.msg.reply('cannot convert `{}` to `int`'.format(S(reminder)))
                
                r = Reminder.select(Reminder).where(
                    (Reminder.message_id << Reminder.with_message_join((Message.id, )).where(
                        Message.author_id == event.author.id
                    )) & (Reminder.id == reminder)
                ).get()
            except Reminder.DoesNotExist:
                return event.msg.reply('<:{}> cannot find reminder #{}'.format(RED_TICK_EMOJI, reminder))
            
            msg = event.msg.reply('Ok, clear reminder #{}?'.format(reminder))
            msg.chain(False).\
                add_reaction(GREEN_TICK_EMOJI).\
                add_reaction(RED_TICK_EMOJI)

            try:
                mra_event = self.wait_for_event(
                    'MessageReactionAdd',
                    message_id=msg.id,
                    conditional=lambda e: (
                        e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                        e.user_id == event.author.id
                    )).get(timeout=10)
            except gevent.Timeout:
                return
            finally:
                msg.delete()

            if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
                return
            
            Reminder.delete_for_user(event.author.id, r.id)
            return event.msg.reply(':ok_hand: I cleared reminder #{} for you'.format(r.id))

    @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 30:
            return event.msg.reply(':warning: you can only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)):
            return event.msg.reply(':warning: thats too far in the future, I\'ll forget!')

        r = Reminder.create(
            message_id=event.msg.id,
            remind_at=remind_at,
            content=content
        )
        self.reminder_task.set_next_schedule(r.remind_at)
        event.msg.reply(':ok_hand: I\'ll remind you at {} ({}) #{}'.format(
            r.remind_at.isoformat(),
            humanize_duration(r.remind_at - datetime.utcnow()),
            r.id
        ))
    
    # @Plugin.command('list global', '[count:int]', context={'mode': 'global'}, group='r', global_=True)
    @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='r', global_=True)
    @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='remind', global_=True)
    # @Plugin.command('reminders global', '[count:int]', context={'mode': 'global'}, global_=True)
    @Plugin.command('reminders', '[limit:int]', context={'mode': 'server'}, global_=True)
    def cmd_remind_list(self, event, limit=None, mode=None):
        user = event.msg.author
        count = Reminder.count_for_user(user.id)
        avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(
            user.id,
            user.avatar,
        )

        embed = MessageEmbed()
        embed.title = '{} reminder{}'.format(count, '' if count == 1 else 's')

        embed.set_author(name=u'{}#{}'.format(
            user.username,
            user.discriminator,
        ), icon_url=avatar)
        embed.color = get_dominant_colors_user(user, avatar)
        embed.set_footer(text='You can cancel reminders with !r clear [ID]')

        if count == 0:
            embed.description = 'You have no upcoming reminders.'
        else:
            query = Reminder.select(Reminder).where(
                (Reminder.message_id << Reminder.with_message_join((Message.id, )).where(
                    Message.author_id == event.author.id
                )) & (Reminder.remind_at > (datetime.utcnow() + timedelta(seconds=1)))
            ).order_by(Reminder.remind_at).limit(limit)

            for reminder in query:
                time = humanize_duration(reminder.remind_at - datetime.utcnow())
                channel = Message.select().where(Message.id == reminder.message_id).get().channel_id
                channel = self.state.channels.get(channel)

                embed.add_field(
                    name=u'#{} in {}'.format(
                        reminder.id,
                        time
                    ),
                    value=u'[`#{}`](https://discordapp.com/channels/{}/{}/{}) {}'.format(
                        channel.name if channel.type != ChannelType.DM else 'Jetski',
                        channel.guild_id if channel.type != ChannelType.DM else '@me',
                        channel.id,
                        reminder.message_id,
                        S(reminder.content)
                    )
                )

        return event.msg.reply(embed=embed)
Exemplo n.º 7
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        event.msg.reply(random.choice(['heads', 'tails']))

    @Plugin.command('number',
                    '[end:int] [start:int]',
                    group='random',
                    global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            return event.msg.reply(':warning: ending number too big!')

        if end <= start:
            return event.msg.reply(
                ':warning: ending number must be larger than starting number!')

        event.msg.reply(str(random.randint(start, end)))

    @Plugin.command('cat', global_=True)
    def cat(self, event):
        # Sometimes random.cat gives us gifs (smh)
        for _ in range(3):
            try:
                r = requests.get('http://random.cat/meow')
                r.raise_for_status()
            except:
                continue

            url = r.json()['file']
            if not url.endswith('.gif'):
                break
        else:
            return event.msg.reply('404 cat not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('cat.jpg', r.content)])

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            return event.msg.reply(u'Unknown emoji: `{}`'.format(emoji))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(S(name)))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id))

        url = 'https://discordapp.com/api/emojis/{}.png'.format(eid)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields),
                               attachments=[('emoji.png', r.content)])

    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        urls = []

        for emoji in emojis.split(' ')[:5]:
            if EMOJI_RE.match(emoji):
                _, eid = EMOJI_RE.findall(emoji)[0]
                urls.append(
                    'https://discordapp.com/api/emojis/{}.png'.format(eid))
            else:
                urls.append(get_emoji_url(emoji))

        width, height, images = 0, 0, []

        for r in Pool(6).imap(requests.get, urls):
            try:
                r.raise_for_status()
            except requests.HTTPError:
                return

            img = Image.open(BytesIO(r.content))
            height = img.height if img.height > height else height
            width += img.width + 10
            images.append(img)

        image = Image.new('RGBA', (width, height))
        width_offset = 0
        for img in images:
            image.paste(img, (width_offset, 0))
            width_offset += img.width + 10

        combined = BytesIO()
        image.save(combined, 'png', quality=55)
        combined.seek(0)
        return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id).order_by(
                    Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            return event.msg.reply(u"I've never seen {}".format(user))

        event.msg.reply(u'I last saw {} {} ago (at {})'.format(
            user, humanize.naturaldelta(datetime.utcnow() - msg.timestamp),
            msg.timestamp))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append(
                (User.username**u'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim.isdigit():
                queries.append(((User.username == username) &
                                (User.discriminator == int(discrim))))

        users = User.select().where(reduce(operator.or_, queries))
        if len(users) == 0:
            return event.msg.reply(u'No users found for query `{}`'.format(
                S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        return event.msg.reply(
            u'Found the following users for your query: ```{}```'.format(
                u'\n'.join(
                    map(lambda i: u'{} ({})'.format(unicode(i), i.user_id),
                        users[:25]))))

    @Plugin.command('server', '[guild_id:snowflake]', global_=True)
    def server(self, event, guild_id=None):
        guild = self.state.guilds.get(guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('invalid server')

        content = []
        content.append(u'**\u276F Server Information**')

        created_at = to_datetime(guild.id)
        content.append(u'Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_at),
            created_at.isoformat(),
        ))
        content.append(u'Members: {}'.format(len(guild.members)))
        content.append(u'Features: {}'.format(', '.join(guild.features)
                                              or 'none'))

        content.append(u'\n**\u276F Counts**')
        text_count = sum(1 for c in guild.channels.values() if not c.is_voice)
        voice_count = len(guild.channels) - text_count
        content.append(u'Roles: {}'.format(len(guild.roles)))
        content.append(u'Text: {}'.format(text_count))
        content.append(u'Voice: {}'.format(voice_count))

        content.append(u'\n**\u276F Members**')
        status_counts = defaultdict(int)
        for member in guild.members.values():
            if not member.user.presence:
                status = Status.OFFLINE
            else:
                status = member.user.presence.status
            status_counts[status] += 1

        for status, count in sorted(status_counts.items(),
                                    key=lambda i: str(i[0]),
                                    reverse=True):
            content.append(u'<{}> - {}'.format(STATUS_EMOJI[status], count))

        embed = MessageEmbed()
        if guild.icon:
            embed.set_thumbnail(url=guild.icon_url)
            embed.color = get_dominant_colors_guild(guild)
        embed.description = '\n'.join(content)
        event.msg.reply('', embed=embed)

    @Plugin.command('info', '<user:user>')
    def info(self, event, user):
        content = []
        content.append(u'**\u276F User Information**')
        content.append(u'ID: {}'.format(user.id))
        content.append(u'Profile: <@{}>'.format(user.id))

        if user.presence:
            emoji, status = get_status_emoji(user.presence)
            content.append('Status: {} <{}>'.format(status, emoji))
            if user.presence.game and user.presence.game.name:
                if user.presence.game.type == GameType.DEFAULT:
                    content.append(u'Game: {}'.format(user.presence.game.name))
                else:
                    content.append(u'Stream: [{}]({})'.format(
                        user.presence.game.name, user.presence.game.url))

        created_dt = to_datetime(user.id)
        content.append('Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_dt),
            created_dt.isoformat()))

        member = event.guild.get_member(user.id) if event.guild else None
        if member:
            content.append(u'\n**\u276F Member Information**')

            if member.nick:
                content.append(u'Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.isoformat(),
            ))

            if member.roles:
                content.append(u'Roles: {}'.format(', '.join(
                    (member.guild.roles.get(r).name for r in member.roles))))

        # Execute a bunch of queries async
        infractions = Infraction.select(
            Infraction.guild_id, fn.COUNT('*')).where(
                (Infraction.user_id == user.id)).group_by(
                    Infraction.guild_id).tuples(). async ()

        # Wait for them all to complete (we're still going to be as slow as the
        #  slowest query, so no need to be smart about this.)
        wait_many(infractions, timeout=10)
        tags = to_tags(guild_id=event.msg.guild.id)

        if infractions.value:
            statsd.timing('sql.duration.infractions',
                          infractions.value._query_time,
                          tags=tags)
            infractions = list(infractions.value)
            total = sum(i[1] for i in infractions)
            content.append(u'\n**\u276F Infractions**')
            content.append('Total Infractions: {}'.format(total))
            content.append('Unique Servers: {}'.format(len(infractions)))

        embed = MessageEmbed()

        avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(
            user.id,
            user.avatar,
        )

        embed.set_author(name=u'{}#{}'.format(
            user.username,
            user.discriminator,
        ),
                         icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        embed.color = get_dominant_colors_user(user, avatar)
        event.msg.reply('', embed=embed)

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))))

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder):
        message = reminder.message_id
        channel = self.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning(
                'Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(
            u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.
            format(
                message.author_id, reminder.created_at,
                humanize.naturaldelta(reminder.created_at - datetime.utcnow()),
                S(reminder.content)))

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id ==
                  GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id
                 )).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            msg.delete_reaction(SNOOZE_EMOJI)
            msg.delete_reaction(GREEN_TICK_EMOJI)

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.')
            return

        reminder.delete_instance()

    @Plugin.command('clear', group='r', global_=True)
    def cmd_remind_clear(self, event):
        count = Reminder.delete_for_user(event.author.id)
        return event.msg.reply(
            ':ok_hand: I cleared {} reminders for you'.format(count))

    @Plugin.command('add',
                    '<duration:str> <content:str...>',
                    group='r',
                    global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 30:
            return event.msg.reply(
                ':warning: you an only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() +
                        timedelta(seconds=5 * YEAR_IN_SEC)):
            return event.msg.reply(
                ':warning: thats too far in the future, I\'ll forget!')

        r = Reminder.create(message_id=event.msg.id,
                            remind_at=remind_at,
                            content=content)
        self.reminder_task.set_next_schedule(r.remind_at)
        event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format(
            r.remind_at.isoformat(),
            humanize.naturaldelta(r.remind_at - datetime.utcnow()),
        ))
Exemplo n.º 8
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        event.msg.reply(random.choice(['heads', 'tails']))

    @Plugin.command('number',
                    '[end:int] [start:int]',
                    group='random',
                    global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            return event.msg.reply(':warning: ending number too big!')

        if end <= start:
            return event.msg.reply(
                ':warning: ending number must be larger than starting number!')

        event.msg.reply(str(random.randint(start, end)))

    @Plugin.command('cat', global_=True)
    def cat(self, event):
        # Sometimes random.cat gives us gifs (smh)
        for _ in range(3):
            try:
                r = requests.get('http://random.cat/meow')
                r.raise_for_status()
            except:
                continue

            url = r.json()['file']
            if not url.endswith('.gif'):
                break
        else:
            return event.msg.reply('404 cat not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('cat.jpg', r.content)])

    @Plugin.command('urban', '<term:str...>', global_=True)
    def urban(self, event, term):
        r = requests.get('http://api.urbandictionary.com/v0/define',
                         params={
                             'term': term,
                         })
        r.raise_for_status()
        data = r.json()

        if not len(data['list']):
            return event.msg.reply(':warning: no matches')

        event.msg.reply('{} - {}'.format(
            data['list'][0]['word'],
            data['list'][0]['definition'],
        ))

    @Plugin.command('pwnd', '<email:str>', global_=True)
    def pwnd(self, event, email):
        r = requests.get(
            'https://haveibeenpwned.com/api/v2/breachedaccount/{}'.format(
                email))

        if r.status_code == 404:
            return event.msg.reply(
                ":white_check_mark: you haven't been pwnd yet, awesome!")

        r.raise_for_status()
        data = r.json()

        sites = []

        for idx, site in enumerate(data):
            sites.append('{} - {} ({})'.format(
                site['BreachDate'],
                site['Title'],
                site['Domain'],
            ))

        return event.msg.reply(
            ":warning: You've been pwnd on {} sites:\n{}".format(
                len(sites),
                '\n'.join(sites),
            ))

    @Plugin.command('geoip', '<ip:str>', global_=True)
    def geoip(self, event, ip):
        r = requests.get('http://json.geoiplookup.io/{}'.format(ip))
        r.raise_for_status()
        data = r.json()

        event.msg.reply(u'{} - {}, {} ({}) | {}, {}'.format(
            data['isp'],
            data['city'],
            data['region'],
            data['country_code'],
            data['latitude'],
            data['longitude'],
        ))

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            return event.msg.reply(u'Unknown emoji: `{}`'.format(emoji))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(name))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(guild.name, guild.id))

        url = 'https://discordapp.com/api/emojis/{}.png'.format(eid)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields),
                               attachments=[('emoji.png', r.content)])

    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        urls = []

        for emoji in emojis.split(' ')[:5]:
            if EMOJI_RE.match(emoji):
                _, eid = EMOJI_RE.findall(emoji)[0]
                urls.append(
                    'https://discordapp.com/api/emojis/{}.png'.format(eid))
            else:
                urls.append(get_emoji_url(emoji))

        width, height, images = 0, 0, []

        for r in Pool(6).imap(requests.get, urls):
            try:
                r.raise_for_status()
            except requests.HTTPError:
                return

            img = Image.open(BytesIO(r.content))
            height = img.height if img.height > height else height
            width += img.width + 10
            images.append(img)

        image = Image.new('RGBA', (width, height))
        width_offset = 0
        for img in images:
            image.paste(img, (width_offset, 0))
            width_offset += img.width + 10

        combined = BytesIO()
        image.save(combined, 'png', quality=55)
        combined.seek(0)
        return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id).order_by(
                    Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            return event.msg.reply(u"I've never seen {}".format(user))

        event.msg.reply(u'I last saw {} {} ({})'.format(
            user, humanize.naturaldelta(datetime.utcnow() - msg.timestamp),
            msg.timestamp))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append(
                (User.username**u'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim.isdigit():
                queries.append(((User.username == username) &
                                (User.discriminator == int(discrim))))

        users = User.select().where(reduce(operator.or_, queries))
        if len(users) == 0:
            return event.msg.reply(u'No users found for query `{}`'.format(
                S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        return event.msg.reply(
            u'Found the following users for your query: ```{}```'.format(
                u'\n'.join(
                    map(lambda i: u'{} ({})'.format(unicode(i), i.user_id),
                        users[:25]))))

    @Plugin.command('server', '[guild_id:snowflake]', global_=True)
    def server(self, event, guild_id=None):
        guild = self.state.guilds.get(guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('invalid server')

        content = []
        content.append(u'**\u276F Server Information**')

        created_at = to_datetime(guild.id)
        content.append(u'Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_at),
            created_at.isoformat(),
        ))
        content.append(u'Members: {}'.format(len(guild.members)))
        content.append(u'Features: {}'.format(', '.join(guild.features)
                                              or 'none'))

        content.append(u'\n**\u276F Counts**')
        text_count = sum(1 for c in guild.channels.values() if not c.is_voice)
        voice_count = len(guild.channels) - text_count
        content.append(u'Roles: {}'.format(len(guild.roles)))
        content.append(u'Text: {}'.format(text_count))
        content.append(u'Voice: {}'.format(voice_count))

        content.append(u'\n**\u276F Members**')
        status_counts = defaultdict(int)
        for member in guild.members.values():
            if not member.user.presence:
                status = Status.OFFLINE
            else:
                status = member.user.presence.status
            status_counts[status] += 1

        for status, count in sorted(status_counts.items(),
                                    key=lambda i: str(i[0]),
                                    reverse=True):
            content.append(u'<{}> - {}'.format(STATUS_EMOJI[status], count))

        embed = MessageEmbed()
        if guild.icon:
            embed.set_thumbnail(url=guild.icon_url)
            embed.color = get_dominant_colors_guild(guild)
        embed.description = '\n'.join(content)
        event.msg.reply('', embed=embed)

    @Plugin.command('info', '<user:user>', global_=True)
    def info(self, event, user):
        content = []
        content.append(u'**\u276F User Information**')

        if user.presence:
            emoji, status = get_status_emoji(user.presence)
            content.append('Status: {} <{}>'.format(status, emoji))
            if user.presence.game and user.presence.game.name:
                if user.presence.game.type == GameType.DEFAULT:
                    content.append(u'Game: {}'.format(user.presence.game.name))
                else:
                    content.append(u'Stream: [{}]({})'.format(
                        user.presence.game.name, user.presence.game.url))

        created_dt = to_datetime(user.id)
        content.append('Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_dt),
            created_dt.isoformat()))

        member = event.guild.get_member(user.id) if event.guild else None
        if member:
            content.append(u'\n**\u276F Member Information**')

            if member.nick:
                content.append(u'Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.isoformat(),
            ))

            if member.roles:
                content.append(u'Roles: {}'.format(', '.join(
                    (member.guild.roles.get(r).name for r in member.roles))))

        try:
            newest_msg = Message.select(Message.timestamp).where(
                (Message.author_id == user.id)
                & (Message.guild_id == event.guild.id)).order_by(
                    Message.timestamp.desc()).get()

            oldest_msg = Message.select(Message.timestamp).where(
                (Message.author_id == user.id)
                & (Message.guild_id == event.guild.id)).order_by(
                    Message.timestamp.asc()).get()
            content.append(u'\n **\u276F Activity**')
            content.append('Last Message: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() -
                                      newest_msg.timestamp),
                newest_msg.timestamp.isoformat(),
            ))
            content.append('First Message: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() -
                                      oldest_msg.timestamp),
                oldest_msg.timestamp.isoformat(),
            ))
        except Message.DoesNotExist:
            pass

        infractions = list(
            Infraction.select(Infraction.guild_id, fn.COUNT('*')).where(
                (Infraction.user_id == user.id)).group_by(
                    Infraction.guild_id).tuples())

        if infractions:
            total = sum(i[1] for i in infractions)
            content.append(u'\n**\u276F Infractions**')
            content.append('Total Infractions: {}'.format(total))
            content.append('Unique Servers: {}'.format(len(infractions)))

        voice = list(
            GuildVoiceSession.select(
                GuildVoiceSession.user_id, fn.COUNT('*'),
                fn.SUM(GuildVoiceSession.ended_at -
                       GuildVoiceSession.started_at)).where(
                           (GuildVoiceSession.user_id == user.id)
                           & (~(GuildVoiceSession.ended_at >> None))).group_by(
                               GuildVoiceSession.user_id).tuples())

        if voice:
            content.append(u'\n**\u276F Voice**')
            content.append(u'Sessions: {}'.format(voice[0][1]))
            content.append(u'Time: {}'.format(
                humanize.naturaldelta(voice[0][2])))

        embed = MessageEmbed()

        avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(
            user.id,
            user.avatar,
        )

        embed.set_author(name=u'{}#{} (<@{}>)'.format(
            user.username,
            user.discriminator,
            user.id,
        ),
                         icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        embed.color = get_dominant_colors_user(user, avatar)
        event.msg.reply('', embed=embed)

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))))

        for reminder in reminders:
            message = reminder.message_id
            channel = self.state.channels.get(message.channel_id)
            if not channel:
                self.log.warning(
                    'Not triggering reminder, channel %s was not found!',
                    message.channel_id)
                continue

            channel.send_message(
                u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.
                format(
                    message.author_id, reminder.created_at,
                    humanize.naturaldelta(reminder.created_at -
                                          datetime.utcnow()),
                    S(reminder.content)))

            reminder.delete_instance()

        self.queue_reminders()

    @Plugin.command('clear', group='r', global_=True)
    def cmd_remind_clear(self, event):
        count = Reminder.delete_for_user(event.author.id)
        return event.msg.reply(
            ':ok_hand: I cleared {} reminders for you'.format(count))

    @Plugin.command('add',
                    '<duration:str> <content:str...>',
                    group='r',
                    global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 30:
            return event.msg.reply(
                ':warning: you an only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() +
                        timedelta(seconds=5 * YEAR_IN_SEC)):
            return event.msg.reply(
                ':warning: thats too far in the future, I\'ll forget!')

        r = Reminder.create(message_id=event.msg.id,
                            remind_at=remind_at,
                            content=content)
        self.reminder_task.set_next_schedule(r.remind_at)
        event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format(
            r.remind_at.isoformat(),
            humanize.naturaldelta(r.remind_at - datetime.utcnow()),
        ))
Exemplo n.º 9
0
class RemindersPlugin(Plugin):
    def load(self, ctx):
        super(RemindersPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))))

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder):
        message = reminder.message_id
        channel = self.bot.client.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning(
                'Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(
            '<@{}> you asked me at {} ({} ago) to remind you about: {}'.format(
                message.author_id, reminder.created_at,
                humanize_duration(reminder.created_at - datetime.utcnow()),
                S(reminder.content)))

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id ==
                  GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id
                 )).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            msg.delete_reaction(SNOOZE_EMOJI)
            msg.delete_reaction(GREEN_TICK_EMOJI)

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit('Ok, I\'ve snoozed that reminder for 20 minutes.')
            return

        reminder.delete_instance()

    @Plugin.command(
        'delete global',
        '[reminder:str]',
        group='reminder',
        aliases=['remove global global', 'clean global', 'clear global'],
        context={'mode': 'global'},
        global_=True)
    @Plugin.command('delete global',
                    '[reminder:str]',
                    group='r',
                    aliases=['remove global', 'clean', 'clear global'],
                    context={'mode': 'global'},
                    global_=True)
    @Plugin.command('delete',
                    '[reminder:str]',
                    group='reminder',
                    aliases=['remove', 'clean', 'clear'],
                    context={'mode': 'server'},
                    global_=True)
    @Plugin.command('delete',
                    '[reminder:str]',
                    group='r',
                    aliases=['remove', 'clean', 'clear'],
                    context={'mode': 'server'},
                    global_=True)
    def cmd_remind_clear(self, event, reminder='all', mode='server'):
        if reminder == 'all':
            count = Reminder.count_for_user(
                event.author.id, event.guild.id
            ) if mode == 'server' else Reminder.count_for_user(event.author.id)

            if Reminder.count_for_user(event.author.id) == 0:
                return event.msg.reply(
                    '<:{}> cannot clear reminders when you don\'t have any'.
                    format(RED_TICK_EMOJI))

            msg = event.msg.reply('Ok, clear {} reminders?'.format(count))
            msg.chain(False).\
                add_reaction(GREEN_TICK_EMOJI).\
                add_reaction(RED_TICK_EMOJI)

            try:
                mra_event = self.wait_for_event(
                    'MessageReactionAdd',
                    message_id=msg.id,
                    conditional=lambda e:
                    (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                     e.user_id == event.author.id)).get(timeout=10)
            except gevent.Timeout:
                return
            finally:
                msg.delete()

            if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
                return

            count = Reminder.delete_all_for_user(
                event.author.id, event.guild.id
            ) if mode == 'server' else Reminder.delete_all_for_user(
                event.author.id)
            return event.msg.reply(
                ':ok_hand: I cleared {} reminders for you'.format(count))
        else:
            try:
                # stupid catch because python sucks
                try:
                    reminder = int(reminder)
                except:
                    return event.msg.reply(
                        'cannot convert `{}` to `int`'.format(S(reminder)))

                r = Reminder.select(Reminder).where(
                    (Reminder.message_id << Reminder.with_message_join(
                        (Message.id, )).where(
                            Message.author_id == event.author.id))
                    & (Reminder.id == reminder)).get()
            except Reminder.DoesNotExist:
                return event.msg.reply('<:{}> cannot find reminder #{}'.format(
                    RED_TICK_EMOJI, reminder))

            msg = event.msg.reply('Ok, clear reminder #{}?'.format(reminder))
            msg.chain(False).\
                add_reaction(GREEN_TICK_EMOJI).\
                add_reaction(RED_TICK_EMOJI)

            try:
                mra_event = self.wait_for_event(
                    'MessageReactionAdd',
                    message_id=msg.id,
                    conditional=lambda e:
                    (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                     e.user_id == event.author.id)).get(timeout=10)
            except gevent.Timeout:
                return
            finally:
                msg.delete()

            if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
                return

            Reminder.delete_for_user(event.author.id, r.id)
            return event.msg.reply(
                ':ok_hand: I cleared reminder #{} for you'.format(r.id))

    @Plugin.command('add',
                    '<duration:str> <content:str...>',
                    group='r',
                    global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 30:
            return event.msg.reply(
                ':warning: you can only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() +
                        timedelta(seconds=5 * YEAR_IN_SEC)):
            return event.msg.reply(
                ':warning: thats too far in the future, I\'ll forget!')

        r = Reminder.create(message_id=event.msg.id,
                            remind_at=remind_at,
                            content=content)
        self.reminder_task.set_next_schedule(r.remind_at)
        event.msg.reply(':ok_hand: I\'ll remind you at {} ({}) #{}'.format(
            r.remind_at.isoformat(),
            humanize_duration(r.remind_at - datetime.utcnow()), r.id))

    @Plugin.command('list global',
                    '[limit:int]',
                    context={'mode': 'global'},
                    group='r',
                    global_=True)
    @Plugin.command('list global',
                    '[limit:int]',
                    context={'mode': 'global'},
                    group='remind',
                    global_=True)
    @Plugin.command('reminders global',
                    '[limit:int]',
                    context={'mode': 'global'},
                    global_=True)
    @Plugin.command('list',
                    '[limit:int]',
                    context={'mode': 'server'},
                    group='r',
                    global_=True)
    @Plugin.command('list',
                    '[limit:int]',
                    context={'mode': 'server'},
                    group='remind',
                    global_=True)
    @Plugin.command('reminders',
                    '[limit:int]',
                    context={'mode': 'server'},
                    global_=True)
    def cmd_remind_list(self, event, limit=None, mode='server'):
        user = event.msg.author
        count = Reminder.count_for_user(user.id, event.guild.id)
        total_count = Reminder.count_for_user(user.id)

        embed = MessageEmbed()
        embed.title = '{} reminder{} ({} total)'.format(
            count if mode == 'server' else total_count, 's' if
            (count != 1 and mode == 'server') or
            (total_count != 1 and mode == 'global') else '', total_count)

        embed.set_author(name=user, icon_url=user.avatar_url)
        embed.color = get_dominant_colors_user(user,
                                               user.get_avatar_url('png'))
        embed.set_footer(text='You can cancel reminders with !r clear [ID]')

        if (count == 0 and mode == 'server') or total_count == 0:
            embed.description = 'You have no upcoming reminders{}.'.format(
                ' in this server. Use `!r list global` to list all your upcoming reminders'
                if total_count > 0 else '')
        else:
            query = Reminder.select(Reminder).where(
                (Reminder.message_id << Reminder.with_message_join(
                    (Message.id, )).where(
                        (Message.author_id == event.author.id) &
                        (Message.guild_id == event.guild.id if mode ==
                         'server' else True)))
                & (Reminder.remind_at >
                   (datetime.utcnow() + timedelta(seconds=1)))).order_by(
                       Reminder.remind_at).limit(limit)

            for reminder in query:
                time = humanize_duration(reminder.remind_at -
                                         datetime.utcnow())
                channel = Message.select().where(
                    Message.id == reminder.message_id).get().channel_id
                channel = self.bot.client.state.channels.get(channel)

                embed.add_field(
                    name='#{} in {}'.format(reminder.id, time),
                    value='[`#{}`](https://discordapp.com/channels/{}/{}/{}) {}'
                    .format(
                        channel.name if channel.type != ChannelType.DM else
                        'Jetski', channel.guild_id
                        if channel.type != ChannelType.DM else '@me',
                        channel.id, reminder.message_id, S(reminder.content)))

        return event.msg.reply(embed=embed)
Exemplo n.º 10
0
    def load(self, ctx):
        super(InfractionsPlugin, self).load(ctx)

        self.inf_task = Eventual(self.clear_infractions)
        self.spawn_later(5, self.queue_infractions)
Exemplo n.º 11
0
class InfractionsPlugin(Plugin):
    def load(self, ctx):
        super(InfractionsPlugin, self).load(ctx)

        self.inf_task = Eventual(self.clear_infractions)
        self.spawn_later(5, self.queue_infractions)

    def queue_infractions(self):
        next_infraction = list(Infraction.select().where(
            (Infraction.active == 1) &
            (~(Infraction.expires_at >> None))
        ).order_by(Infraction.expires_at.asc()).limit(1))

        if not next_infraction:
            self.log.info('[INF] no infractions to wait for')
            return

        self.log.info('[INF] waiting until %s for %s', next_infraction[0].expires_at, next_infraction[0].id)
        self.inf_task.set_next_schedule(next_infraction[0].expires_at)

    def clear_infractions(self):
        expired = list(Infraction.select().where(
            (Infraction.active == 1) &
            (Infraction.expires_at < datetime.utcnow())
        ))

        self.log.info('[INF] attempting to clear %s expired infractions', len(expired))

        for item in expired:
            guild = self.state.guilds.get(item.guild_id)
            if not guild:
                self.log.warning('[INF] failed to clear infraction %s, no guild exists', item.id)
                continue

            # TODO: hacky
            type_ = {i.index: i for i in Infraction.Types.attrs}[item.type_]
            if type_ == Infraction.Types.TEMPBAN:
                self.call(
                    'ModLogPlugin.create_debounce',
                    guild.id,
                    ['GuildBanRemove'],
                    user_id=item.user_id,
                )

                guild.delete_ban(item.user_id)

                # TODO: perhaps join on users above and use username from db
                self.call(
                    'ModLogPlugin.log_action_ext',
                    Actions.MEMBER_TEMPBAN_EXPIRE,
                    guild.id,
                    user_id=item.user_id,
                    user=unicode(self.state.users.get(item.user_id) or item.user_id),
                    inf=item
                )
            elif type_ == Infraction.Types.TEMPMUTE or Infraction.Types.TEMPROLE:
                member = guild.get_member(item.user_id)
                if member:
                    if item.metadata['role'] in member.roles:
                        self.call(
                            'ModLogPlugin.create_debounce',
                            guild.id,
                            ['GuildMemberUpdate'],
                            user_id=item.user_id,
                            role_id=item.metadata['role'],
                        )

                        member.remove_role(item.metadata['role'])

                        self.call(
                            'ModLogPlugin.log_action_ext',
                            Actions.MEMBER_TEMPMUTE_EXPIRE,
                            guild.id,
                            member=member,
                            inf=item
                        )
                else:
                    GuildMemberBackup.remove_role(
                        item.guild_id,
                        item.user_id,
                        item.metadata['role'])
            else:
                self.log.warning('[INF] failed to clear infraction %s, type is invalid %s', item.id, item.type_)
                continue

            # TODO: n+1
            item.active = False
            item.save()

        # Wait a few seconds to backoff from a possible bad loop, and requeue new infractions
        gevent.sleep(5)
        self.queue_infractions()

    @Plugin.listen('GuildMemberUpdate', priority=Priority.BEFORE)
    def on_guild_member_update(self, event):
        pre_member = event.guild.members.get(event.id)
        if not pre_member:
            return

        pre_roles = set(pre_member.roles)
        post_roles = set(event.roles)
        if pre_roles == post_roles:
            return

        removed = pre_roles - post_roles

        # If the user was unmuted, mark any temp-mutes as inactive
        if event.config.mute_role in removed:
            Infraction.clear_active(event, event.user.id, [Infraction.Types.TEMPMUTE])

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        Infraction.clear_active(event, event.user.id, [Infraction.Types.BAN, Infraction.Types.TEMPBAN])

    @Plugin.command('unban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD)
    def unban(self, event, user, reason=None):
        try:
            GuildBan.get(user_id=user, guild_id=event.guild.id)
            event.guild.delete_ban(user)
        except GuildBan.DoesNotExist:
            raise CommandFail('user with id `{}` is not banned'.format(user))

        Infraction.create(
            guild_id=event.guild.id,
            user_id=user,
            actor_id=event.author.id,
            type_=Infraction.Types.UNBAN,
            reason=reason
        )
        raise CommandSuccess('unbanned user with id `{}`'.format(user))

    @Plugin.command('archive', group='infractions', level=CommandLevels.ADMIN)
    def infractions_archive(self, event):
        user = User.alias()
        actor = User.alias()

        q = Infraction.select(Infraction, user, actor).join(
            user,
            on=((Infraction.user_id == user.user_id).alias('user'))
        ).switch(Infraction).join(
            actor,
            on=((Infraction.actor_id == actor.user_id).alias('actor'))
        ).where(Infraction.guild_id == event.guild.id)

        buff = StringIO()
        w = csv.writer(buff)

        for inf in q:
            w.writerow([
                inf.id,
                inf.user_id,
                unicode(inf.user).encode('utf-8'),
                inf.actor_id,
                unicode(inf.actor).encode('utf-8'),
                unicode({i.index: i for i in Infraction.Types.attrs}[inf.type_]).encode('utf-8'),
                unicode(inf.reason).encode('utf-8'),
            ])

        event.msg.reply('Ok, here is an archive of all infractions', attachments=[
            ('infractions.csv', buff.getvalue())
        ])

    @Plugin.command('info', '<infraction:int>', group='infractions', level=CommandLevels.MOD)
    def infraction_info(self, event, infraction):
        try:
            user = User.alias()
            actor = User.alias()

            infraction = Infraction.select(Infraction, user, actor).join(
                user,
                on=((Infraction.user_id == user.user_id).alias('user'))
            ).switch(Infraction).join(
                actor,
                on=((Infraction.actor_id == actor.user_id).alias('actor'))
            ).where(
                    (Infraction.id == infraction) &
                    (Infraction.guild_id == event.guild.id)
            ).get()
        except Infraction.DoesNotExist:
            raise CommandFail('cannot find an infraction with ID `{}`'.format(infraction))

        type_ = {i.index: i for i in Infraction.Types.attrs}[infraction.type_]
        embed = MessageEmbed()

        if type_ in (Infraction.Types.MUTE, Infraction.Types.TEMPMUTE, Infraction.Types.TEMPROLE):
            embed.color = 0xfdfd96
        elif type_ in (Infraction.Types.KICK, Infraction.Types.SOFTBAN):
            embed.color = 0xffb347
        else:
            embed.color = 0xff6961

        embed.title = str(type_).title()
        embed.set_thumbnail(url=infraction.user.get_avatar_url())
        embed.add_field(name='User', value=unicode(infraction.user), inline=True)
        embed.add_field(name='Moderator', value=unicode(infraction.actor), inline=True)
        embed.add_field(name='Active', value='yes' if infraction.active else 'no', inline=True)
        if infraction.active and infraction.expires_at:
            embed.add_field(name='Expires', value=humanize.naturaldelta(infraction.expires_at - datetime.utcnow()))
        embed.add_field(name='Reason', value=infraction.reason or '_No Reason Given', inline=False)
        embed.timestamp = infraction.created_at.isoformat()
        event.msg.reply('', embed=embed)

    @Plugin.command('search', '[query:user|str...]', group='infractions', level=CommandLevels.MOD)
    def infraction_search(self, event, query=None):
        q = (Infraction.guild_id == event.guild.id)

        if query and isinstance(query, list) and isinstance(query[0], DiscoUser):
            query = query[0].id
        elif query:
            query = ' '.join(query)

        if query and (isinstance(query, int) or query.isdigit()):
            q &= (
                (Infraction.id == int(query)) |
                (Infraction.user_id == int(query)) |
                (Infraction.actor_id == int(query)))
        elif query:
            q &= (Infraction.reason ** query)

        user = User.alias()
        actor = User.alias()

        infractions = Infraction.select(Infraction, user, actor).join(
            user,
            on=((Infraction.user_id == user.user_id).alias('user'))
        ).switch(Infraction).join(
            actor,
            on=((Infraction.actor_id == actor.user_id).alias('actor'))
        ).where(q).order_by(Infraction.created_at.desc()).limit(6)

        tbl = MessageTable()

        tbl.set_header('ID', 'Created', 'Type', 'User', 'Moderator', 'Active', 'Reason')
        for inf in infractions:
            type_ = {i.index: i for i in Infraction.Types.attrs}[inf.type_]
            reason = inf.reason or ''
            if len(reason) > 256:
                reason = reason[:256] + '...'

            if inf.active:
                active = 'yes'
                if inf.expires_at:
                    active += ' (expires in {})'.format(humanize.naturaldelta(inf.expires_at - datetime.utcnow()))
            else:
                active = 'no'

            tbl.add(
                inf.id,
                inf.created_at.isoformat(),
                str(type_),
                unicode(inf.user),
                unicode(inf.actor),
                active,
                clamp(reason, 128)
            )

        event.msg.reply(tbl.compile())

    @Plugin.command('recent', aliases=['latest'], group='infractions', level=CommandLevels.MOD)
    def infractions_recent(self, event):
        # TODO: f*****g write this bruh
        pass

    @Plugin.command('duration', '<infraction:int> <duration:str>', group='infractions', level=CommandLevels.MOD)
    def infraction_duration(self, event, infraction, duration):
        try:
            inf = Infraction.get(id=infraction)
        except Infraction.DoesNotExist:
            raise CommandFail('invalid infraction (try `!infractions recent`)')

        if inf.actor_id != event.author.id and event.user_level < CommandLevels.ADMIN:
            raise CommandFail('only administrators can modify the duration of infractions created by other moderators')

        if not inf.active:
            raise CommandFail('that infraction is not active and cannot be updated')

        expires_dt = parse_duration(duration, inf.created_at)

        converted = False
        if inf.type_ in [Infraction.Types.MUTE.index, Infraction.Types.BAN.index]:
            inf.type_ = (
                Infraction.Types.TEMPMUTE
                if inf.type_ == Infraction.Types.MUTE.index else
                Infraction.Types.TEMPBAN
            )
            converted = True
        elif inf.type_ not in [
                Infraction.Types.TEMPMUTE.index,
                Infraction.Types.TEMPBAN.index,
                Infraction.Types.TEMPROLE.index]:
            raise CommandFail('cannot set the duration for that type of infraction')

        inf.expires_at = expires_dt
        inf.save()
        self.queue_infractions()

        if converted:
            raise CommandSuccess('ok, I\'ve made that infraction temporary, it will now expire on {}'.format(
                inf.expires_at.isoformat()
            ))
        else:
            raise CommandSuccess('ok, I\'ve updated that infractions duration, it will now expire on {}'.format(
                inf.expires_at.isoformat()
            ))

    @Plugin.command('reason', '<infraction:int> <reason:str...>', group='infractions', level=CommandLevels.MOD)
    def reason(self, event, infraction, reason):
        try:
            inf = Infraction.get(id=infraction)
        except Infraction.DoesNotExist:
            inf = None

        if inf is None or inf.guild_id != event.guild.id:
            event.msg.reply('Unknown infraction ID')
            return

        if not inf.actor_id:
            inf.actor_id = event.author.id

        if inf.actor_id != event.author.id and event.user_level < event.config.reason_edit_level:
            raise CommandFail('you do not have the permissions required to edit other moderators infractions')

        inf.reason = reason
        inf.save()

        raise CommandSuccess('I\'ve updated the reason for infraction #{}'.format(inf.id))

    def can_act_on(self, event, victim_id, throw=True):
        if event.author.id == victim_id:
            if not throw:
                return False
            raise CommandFail('cannot execute that action on yourself')

        victim_level = self.bot.plugins.get('CorePlugin').get_level(event.guild, victim_id)

        if event.user_level <= victim_level:
            if not throw:
                return False
            raise CommandFail('invalid permissions')

        return True

    def confirm_action(self, event, message):
        if not event.config.confirm_actions:
            return

        if event.config.confirm_actions_reaction:
            event.msg.add_reaction(GREEN_TICK_EMOJI)
            return

        msg = event.msg.reply(message)

        if event.config.confirm_actions_expiry > 0:
            # Close over this thread local
            expiry = event.config.confirm_actions_expiry

            def f():
                gevent.sleep(expiry)
                msg.delete()

            # Run this in a greenlet so we dont block event execution
            self.spawn(f)

    @Plugin.command('mute', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD)
    @Plugin.command('tempmute', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD)
    def tempmute(self, event, user, duration=None, reason=None):
        if not duration and reason:
            duration = parse_duration(reason.split(' ')[0], safe=True)
            if duration:
                if ' ' in reason:
                    reason = reason.split(' ', 1)[-1]
                else:
                    reason = None
        elif duration:
            duration = parse_duration(duration)

        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            if not event.config.mute_role:
                raise CommandFail('mute is not setup on this server')

            if event.config.mute_role in member.roles:
                raise CommandFail(u'{} is already muted'.format(member.user))

            # If we have a duration set, this is a tempmute
            if duration:
                # Create the infraction
                Infraction.tempmute(self, event, member, reason, duration)
                self.queue_infractions()

                self.confirm_action(event, maybe_string(
                    reason,
                    u':ok_hand: {u} is now muted for {t} (`{o}`)',
                    u':ok_hand: {u} is now muted for {t}',
                    u=member.user,
                    t=humanize.naturaldelta(duration - datetime.utcnow()),
                ))
            else:
                existed = False
                # If the user is already muted check if we can take this from a temp
                #  to perma mute.
                if event.config.mute_role in member.roles:
                    existed = Infraction.clear_active(event, member.id, [Infraction.Types.TEMPMUTE])

                    # The user is 100% muted and not tempmuted at this point, so lets bail
                    if not existed:
                        raise CommandFail(u'{} is already muted'.format(member.user))

                Infraction.mute(self, event, member, reason)

                existed = u' [was temp-muted]' if existed else ''
                self.confirm_action(event, maybe_string(
                    reason,
                    u':ok_hand: {u} is now muted (`{o}`)' + existed,
                    u':ok_hand: {u} is now muted' + existed,
                    u=member.user,
                ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command(
        'temprole',
        '<user:user|snowflake> <role:snowflake|str> <duration:str> [reason:str...]',
        level=CommandLevels.MOD)
    def temprole(self, event, user, role, duration, reason=None):
        member = event.guild.get_member(user)
        if not member:
            raise CommandFail('invalid user')

        self.can_act_on(event, member.id)
        role_id = role if isinstance(role, (int, long)) else event.config.role_aliases.get(role.lower())
        if not role_id or role_id not in event.guild.roles:
            raise CommandFail('invalid or unknown role')

        if role_id in member.roles:
            raise CommandFail(u'{} is already in that role'.format(member.user))

        expire_dt = parse_duration(duration)
        Infraction.temprole(self, event, member, role_id, reason, expire_dt)
        self.queue_infractions()

        self.confirm_action(event, maybe_string(
            reason,
            u':ok_hand: {u} is now in the {r} role for {t} (`{o}`)',
            u':ok_hand: {u} is now in the {r} role for {t}',
            r=event.guild.roles[role_id].name,
            u=member.user,
            t=humanize.naturaldelta(expire_dt - datetime.utcnow()),
        ))

    @Plugin.command('unmute', '<user:user|snowflake>', level=CommandLevels.MOD)
    def unmute(self, event, user, reason=None):
        # TOOD: eventually we should pull the role from the GuildMemberBackup if they arent in server
        member = event.guild.get_member(user)

        if member:
            self.can_act_on(event, member.id)
            if not event.config.mute_role:
                raise CommandFail('mute is not setup on this server')

            if event.config.mute_role not in member.roles:
                raise CommandFail(u'{} is not muted'.format(member.user))

            Infraction.clear_active(event, member.id, [Infraction.Types.MUTE, Infraction.Types.TEMPMUTE])

            self.call(
                'ModLogPlugin.create_debounce',
                event,
                ['GuildMemberUpdate'],
                role_id=event.config.mute_role,
            )

            member.remove_role(event.config.mute_role)

            self.call(
                'ModLogPlugin.log_action_ext',
                Actions.MEMBER_UNMUTED,
                event.guild.id,
                member=member,
                actor=unicode(event.author) if event.author.id != member.id else 'Automatic',
            )

            self.confirm_action(event, u':ok_hand: {} is now unmuted'.format(member.user))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('kick', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD)
    def kick(self, event, user, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            Infraction.kick(self, event, member, reason)
            self.confirm_action(event, maybe_string(
                reason,
                u':ok_hand: kicked {u} (`{o}`)',
                u':ok_hand: kicked {u}',
                u=member.user,
            ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('mkick', parser=True, level=CommandLevels.MOD)
    @Plugin.parser.add_argument('users', type=long, nargs='+')
    @Plugin.parser.add_argument('-r', '--reason', default='', help='reason for modlog')
    def mkick(self, event, args):
        members = []
        for user_id in args.users:
            member = event.guild.get_member(user_id)
            if not member:
                # TODO: this sucks, batch these
                raise CommandFail('failed to kick {}, user not found'.format(user_id))

            if not self.can_act_on(event, member.id, throw=False):
                raise CommandFail('failed to kick {}, invalid permissions'.format(user_id))

            members.append(member)

        msg = event.msg.reply('Ok, kick {} users for `{}`?'.format(len(members), args.reason or 'no reason'))
        msg.chain(False).\
            add_reaction(GREEN_TICK_EMOJI).\
            add_reaction(RED_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e: (
                    e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                    e.user_id == event.author.id
                )).get(timeout=10)
        except gevent.Timeout:
            return
        finally:
            msg.delete()

        if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
            return

        for member in members:
            Infraction.kick(self, event, member, args.reason)

        raise CommandSuccess('kicked {} users'.format(len(members)))

    @Plugin.command('ban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD)
    @Plugin.command('forceban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD)
    def ban(self, event, user, reason=None):
        member = None

        if isinstance(user, (int, long)):
            self.can_act_on(event, user)
            Infraction.ban(self, event, user, reason, guild=event.guild)
        else:
            member = event.guild.get_member(user)
            if member:
                self.can_act_on(event, member.id)
                Infraction.ban(self, event, member, reason, guild=event.guild)
            else:
                raise CommandFail('invalid user')

        self.confirm_action(event, maybe_string(
            reason,
            u':ok_hand: banned {u} (`{o}`)',
            u':ok_hand: banned {u}',
            u=member.user if member else user,
        ))

    @Plugin.command('mban', parser=True, level=CommandLevels.MOD)
    @Plugin.parser.add_argument('users', type=long, nargs='+')
    @Plugin.parser.add_argument('-r', '--reason', default='', help='reason for modlog')
    def mban(self, event, args):
        members = []
        for user_id in args.users:
            member = event.guild.get_member(user_id)
            if not member:
                raise CommandFail('failed to ban {}, user not found'.format(user_id))

            if not self.can_act_on(event, member.id, throw=False):
                raise CommandFail('failed to ban {}, invalid permissions'.format(user_id))

            members.append(member)

        msg = event.msg.reply('Ok, ban {} users for `{}`?'.format(len(members), args.reason or 'no reason'))
        msg.chain(False).\
            add_reaction(GREEN_TICK_EMOJI).\
            add_reaction(RED_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e: (
                    e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                    e.user_id == event.author.id
                )).get(timeout=10)
        except gevent.Timeout:
            return
        finally:
            msg.delete()

        if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
            return

        for member in members:
            Infraction.ban(self, event, member, args.reason, guild=event.guild)

        raise CommandSuccess('banned {} users'.format(len(members)))
		
    @Plugin.command('softban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD)
    def softban(self, event, user, reason=None):
        """
        Ban then unban a user from the server (with an optional reason for the modlog)
        """
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            Infraction.softban(self, event, member, reason)
            self.confirm_action(event, maybe_string(
                reason,
                u':ok_hand: soft-banned {u} (`{o}`)',
                u':ok_hand: soft-banned {u}',
                u=member.user,
            ))
        else:
            raise CommandFail('invald user')

    @Plugin.command('tempban', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD)
    def tempban(self, event, duration, user, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            expires_dt = parse_duration(duration)
            Infraction.tempban(self, event, member, reason, expires_dt)
            self.queue_infractions()
            self.confirm_action(event, maybe_string(
                reason,
                u':ok_hand: temp-banned {u} for {t} (`{o}`)',
                u':ok_hand: temp-banned {u} for {t}',
                u=member.user,
                t=humanize.naturaldelta(expires_dt - datetime.utcnow()),
            ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('warn', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD)
    def warn(self, event, user, reason=None):
        member = None

        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            Infraction.warn(self, event, member, reason, guild=event.guild)
        else:
            raise CommandFail('invalid user')

        self.confirm_action(event, maybe_string(
            reason,
            u':ok_hand: warned {u} (`{o}`)',
            u':ok_hand: warned {u}',
            u=member.user if member else user,
        ))
Exemplo n.º 12
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        raise CommandSuccess(random.choice(['heads', 'tails']))

    @Plugin.command('number',
                    '[end:int] [start:int]',
                    group='random',
                    global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            raise CommandSuccess('Ending number too big!')

        if end <= start:
            raise CommandSuccess(
                'Ending number must be larger than starting number!')

        raise CommandSuccess(str(random.randint(start, end)))

    @Plugin.command('cat', '{bentley}', global_=True)
    def cat(self, event, bentley=False):
        try:
            if bentley:
                URL = 'https://bentley.tadhg.sh/api/random'
                data = requests.get(URL).json()
                fname = 'bentley-' + str(
                    data['id'])  # Probably don't have to, but gonna.
                cat = requests.get(data['url'])
            else:
                URL = 'https://api.thecatapi.com/v1/images/search'
                data = requests.get(URL).json()
                fname = data[0]['id']
                cat = requests.get(data[0]['url'])
            cat.raise_for_status()
            fext = cat.headers['content-type'].split('/')[-1].split(';')[0]
            event.msg.reply('',
                            attachments=[('cat-{}.{}'.format(fname, fext),
                                          cat.content)])
        except:
            return event.msg.reply('{} Cat not found :('.format(
                cat.status_code))

    @Plugin.command('otter', global_=True)
    def otter(self, event):
        try:
            URL = 'https://otter.bruhmomentlol.repl.co/random'
            otter = requests.get(URL)
            otter.raise_for_status()

            fext = otter.headers['x-file-ext']
            event.msg.reply('',
                            attachments=[('otter.{}'.format(fext),
                                          otter.content)])
        except:
            return event.msg.reply('{} Otter not found :('.format(
                otter.status_code))

    @Plugin.command('dog', global_=True)
    def dog(self, event):
        try:
            URL = 'https://api.thedogapi.com/v1/images/search'
            data = requests.get(URL).json()
            dog = requests.get(data[0]['url'])
            dog.raise_for_status()

            fname = data[0]['id']
            fext = dog.headers['content-type'].split('/')[-1].split(';')[0]
            event.msg.reply('',
                            attachments=[('dog-{}.{}'.format(fname, fext),
                                          dog.content)])
        except Exception as e:
            return event.msg.reply('{} Dog not found :('.format(
                dog.status_code))

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            raise CommandFail('Unknown emoji: `{}`'.format(emoji))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(S(name)))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id))

        url = 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields),
                               attachments=[('emoji.png', r.content)])

    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        urls = []

        for emoji in emojis.split(' ')[:5]:
            if EMOJI_RE.match(emoji):
                _, eid = EMOJI_RE.findall(emoji)[0]
                urls.append(
                    'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid))
            else:
                urls.append(get_emoji_url(emoji))

        width, height, images = 0, 0, []

        for r in Pool(6).imap(requests.get, urls):
            try:
                r.raise_for_status()
            except requests.HTTPError:
                return

            img = Image.open(BytesIO(r.content))
            height = img.height if img.height > height else height
            width += img.width + 10
            images.append(img)

        image = Image.new('RGBA', (width, height))
        width_offset = 0
        for img in images:
            image.paste(img, (width_offset, 0))
            width_offset += img.width + 10

        combined = BytesIO()
        image.save(combined, 'png', quality=55)
        combined.seek(0)
        return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user: User):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id).order_by(
                    Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            raise CommandFail("I've never seen {}".format(user))

        raise CommandSuccess('I last saw {} {}'.format(
            user, int(msg.timestamp.timestamp())))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query: str):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append(
                (User.username**'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim is not None:
                queries.append(((User.username == username) &
                                (User.discriminator == discrim)))

        users = User.select().where(reduce(operator.or_, queries)).limit(10)
        if len(users) == 0:
            raise CommandFail('No users found for query `{}`'.format(
                S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        raise CommandSuccess(
            'Found the following users for your query: ```{}```'.format(
                '\n'.join([
                    '{} ({})'.format(str(i), i.user_id) for i in users[:25]
                ])))

    @Plugin.command('server',
                    '[guild_id:snowflake]',
                    aliases=['guild'],
                    global_=True)
    def server(self, event, guild_id=None):
        guild: Guild = self.state.guilds.get(
            guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('Invalid server')

        embed = MessageEmbed()
        embed.set_author(
            MessageEmbedAuthor(name=guild.name, icon_url=guild.icon_url()))

        # General Abouts
        about_field = MessageEmbedField()
        about_field.name = '**\u276F About**'
        about_text = 'Created by {} ({}) — <t:{}:R>'.format(
            guild.owner, guild.owner.id,
            int(to_datetime(guild.id).replace(tzinfo=pytz.UTC).timestamp()))
        about_text += '\nMembers: {:,}/{:,}'.format(
            guild.approximate_presence_count, guild.member_count)
        about_text += '\nRegion: {}'.format(guild.region)
        about_field.value = about_text
        embed.add_field(about_field)

        # General Counts
        counts_field = MessageEmbedField()
        counts_field.name = '\n**\u276F Counts**'
        text_count = sum(1 for c in list(guild.channels.values())
                         if not c.is_voice and not c.is_thread)
        voice_count = len(guild.channels) - text_count

        counts_field.value = 'Roles: {:,}\nText: {:,}\nVoice: {:,}'.format(
            len(guild.roles), text_count, voice_count)
        embed.add_field(counts_field)

        # Security
        security_field = MessageEmbedField()
        security_field.name = '\n**\u276F Security**'
        security_field.value = 'Verification: {}\nExplicit Content: {}'.format(
            guild.verification_level, guild.explicit_content_filter)
        embed.add_field(security_field)

        # Features
        features_field = MessageEmbedField()
        features_field.name = '\n**\u276F Features**'
        features_field.value = 'Features: {}'.format(', '.join(guild.features))
        embed.add_field(features_field)

        if guild.icon:
            embed.color = get_dominant_colors_guild(guild)
        event.msg.reply('', embed=embed)

    @Plugin.command('info', '[user:user|snowflake]', aliases='whois')
    def info(self, event, user: User = None):
        if not user:
            user = event.author
        else:
            if not isinstance(user, DiscoUser):
                try:
                    user = self.state.guilds[event.guild.id].members[user].user
                except KeyError:
                    try:
                        user = self.state.users[user]
                    except KeyError:
                        try:
                            user = self.bot.client.api.users_get(user)
                        except APIException:
                            return event.msg.reply(
                                ':eyes: User not found').after(3).delete()

        self.client.api.channels_typing(event.channel.id)

        content = []
        content.append('**\u276F User Information**')
        content.append('Profile: <@{}>'.format(user.id))

        created_dt = to_datetime(user.id)
        content.append('Created: <t:{0}:R> (<t:{0}:f>)'.format(
            int(created_dt.replace(tzinfo=pytz.UTC).timestamp())))

        member = event.guild.get_member(user.id) if event.guild else None

        if user.public_flags:
            badges = ''
            user_badges = list(UserFlags(user.public_flags))
            for badge in user_badges:
                badges += '<{}> '.format(BADGE_EMOJI[badge])

            content.append('Badges: {}'.format(badges))

        if member:
            content.append('\n**\u276F Member Information**')

            if member.nick:
                content.append('Nickname: {}'.format(member.nick))

            content.append('Joined: <t:{0}:R> (<t:{0}:f>)'.format(
                int(member.joined_at.replace(tzinfo=pytz.UTC).timestamp())))

            content.append('Messages: {}'.format(
                int(
                    Message.select(fn.Count(
                        Message.id)).where((Message.author_id == user.id)
                                           & (Message.guild_id == event.guild.
                                              id)).tuples()[0][0])))

            if member.roles:
                content.append('Roles: {}'.format(', '.join(
                    ('<@&{}>'.format(r) for r in member.roles))))

        # Execute a bunch of queries
        newest_msg = Message.select(fn.MAX(Message.id)).where(
            (Message.author_id == user.id)
            & (Message.guild_id == event.guild.id)).tuples()[0][0]

        infractions = Infraction.select(Infraction.id).where(
            (Infraction.user_id == user.id)
            & (Infraction.guild_id == event.guild.id)).tuples()

        if newest_msg:
            content.append('\n **\u276F Activity**')
            content.append('Last Message: <t:{0}:R> (<t:{0}:f>)'.format(
                int((to_datetime(newest_msg).replace(
                    tzinfo=pytz.UTC)).timestamp())))
            # content.append('First Message: {} ({})'.format(
            #    humanize.naturaltime(datetime.utcnow() - to_datetime(oldest_msg)),
            #    to_datetime(oldest_msg).strftime("%b %d %Y %H:%M:%S"),
            # ))

        if len(infractions) > 0:
            content.append('\n**\u276F Infractions**')
            total = len(infractions)
            content.append('Total Infractions: **{:,}**'.format(total))

        embed = MessageEmbed()

        try:
            avatar = User.with_id(user.id).get_avatar_url()
        except APIException:
            avatar = user.get_avatar_url(
            )  # This fails if the user has never been seen by speedboat.

        embed.set_author(name='{} ({})'.format(
            str(user),
            user.id,
        ),
                         icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        embed.color = get_dominant_colors_user(user, avatar)
        event.msg.reply('', embed=embed)

    @Plugin.command('config', global_=True)
    def config_cmd(self, event):
        raise CommandSuccess('{}/guilds/{}/config'.format(
            WEB_URL, event.guild.id))

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))))

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder: Reminder):
        message = Message.get(reminder.message_id)
        channel = self.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning(
                'Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(
            '<@{}> you asked me on <t:{reminder_time}:f> (<t:{reminder_time}:R>) to remind you about: {}'
            .format(
                message.author_id,
                S(reminder.content),
                reminder_time=int(
                    reminder.created_at.replace(tzinfo=pytz.UTC).timestamp()),
            ),
            allowed_mentions={'users': [str(message.author_id)]})

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id ==
                  GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id
                 )).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            try:
                msg.delete_all_reactions()
            except APIException:  # We don't have permission to remove reactions, but, we don't want to fail the reminder.
                pass

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit(
                'Ok, I\'ve snoozed that reminder. You\'ll get another notification in 20 minutes.'
            )
            return

        reminder.delete_instance()

    @Plugin.command('clear', group='r', global_=True)
    def cmd_remind_clear(self, event):
        count = Reminder.delete_for_user(event.author.id)
        raise CommandSuccess('I cleared {} reminders for you'.format(count))

    @Plugin.command('add',
                    '<duration:str> [content:str...]',
                    group='r',
                    global_=True)
    @Plugin.command('remind', '<duration:str> [content:str...]', global_=True)
    def cmd_remind(self, event, duration, content=None):
        if Reminder.count_for_user(event.author.id) > 15:
            raise CommandFail('You can only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() +
                        timedelta(seconds=5 * YEAR_IN_SEC)):
            raise CommandFail('That\'s too far in the future... I\'ll forget!')

        if event.msg.message_reference.message_id:
            referenced_msg: MessageReference = event.channel.get_message(
                event.msg.message_reference.message_id)
            content = 'https://discord.com/channels/{}/{}/{}'.format(
                self.state.channels.get(referenced_msg.channel_id).guild_id,
                referenced_msg.channel_id, referenced_msg.id)
        elif not content:
            raise CommandFail(
                'You need to provide content for the reminder, or reply to a message!'
            )

        r = Reminder.create(message_id=event.msg.id,
                            remind_at=remind_at,
                            content=content)
        self.reminder_task.set_next_schedule(r.remind_at)
        raise CommandSuccess(
            'I\'ll remind you at <t:{0}:f> (<t:{0}:R>)'.format(
                int(r.remind_at.replace(tzinfo=pytz.UTC).timestamp())))
Exemplo n.º 13
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        raise CommandSuccess(random.choice(['heads', 'tails']))

    @Plugin.command('number',
                    '[end:int] [start:int]',
                    group='random',
                    global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            raise CommandSuccess('Ending number too big!')

        if end <= start:
            raise CommandSuccess(
                'Ending number must be larger than starting number!')

        raise CommandSuccess(str(random.randint(start, end)))

    @Plugin.command('cat', global_=True)
    def cat(self, event):
        try:
            r = requests.get(
                'https://api.thecatapi.com/v1/images/search?format=src')
            r.raise_for_status()
            ext = r.headers['content-type'].split('/')[-1].split(';')[0]
            event.msg.reply('',
                            attachments=[('cat.{}'.format(ext), r.content)])
        except:
            return event.msg.reply(r.status_code + ' Cat not found :(')

    @Plugin.command('dog', global_=True)
    def dog(self, event):
        try:
            r = requests.get(
                'https://api.thedogapi.com/v1/images/search?format=src')
            r.raise_for_status()
            ext = r.headers['content-type'].split('/')[-1].split(';')[0]
            event.msg.reply('',
                            attachments=[('dog.{}'.format(ext), r.content)])
        except Exception as e:
            return event.msg.reply(e.with_traceback + ' Dog not found :(')

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            raise CommandFail('Unknown emoji: `{}`'.format(emoji))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(S(name)))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id))

        url = 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields),
                               attachments=[('emoji.png', r.content)])

    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        urls = []

        for emoji in emojis.split(' ')[:5]:
            if EMOJI_RE.match(emoji):
                _, eid = EMOJI_RE.findall(emoji)[0]
                urls.append(
                    'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid))
            else:
                urls.append(get_emoji_url(emoji))

        width, height, images = 0, 0, []

        for r in Pool(6).imap(requests.get, urls):
            try:
                r.raise_for_status()
            except requests.HTTPError:
                return

            img = Image.open(BytesIO(r.content))
            height = img.height if img.height > height else height
            width += img.width + 10
            images.append(img)

        image = Image.new('RGBA', (width, height))
        width_offset = 0
        for img in images:
            image.paste(img, (width_offset, 0))
            width_offset += img.width + 10

        combined = BytesIO()
        image.save(combined, 'png', quality=55)
        combined.seek(0)
        return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id).order_by(
                    Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            raise CommandFail("I've never seen {}".format(user))

        raise CommandSuccess(
            'I last saw {} {} (at {})'.
            format(  #TODO: Prettify this timestamp response like in inf latest
                user, humanize.naturaltime(datetime.utcnow() - msg.timestamp),
                msg.timestamp))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append(
                (User.username**'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim.isdigit():
                queries.append(((User.username == username) &
                                (User.discriminator == int(discrim))))

        users = User.select().where(reduce(operator.or_, queries)).limit(10)
        if len(users) == 0:
            raise CommandFail('No users found for query `{}`'.format(
                S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        raise CommandSuccess(
            'Found the following users for your query: ```{}```'.format(
                '\n'.join([
                    '{} ({})'.format(str(i), i.user_id) for i in users[:25]
                ])))

    @Plugin.command('server', '[guild_id:snowflake]', global_=True)
    def server(self, event, guild_id=None):
        guild = self.state.guilds.get(guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('Invalid server')

        content = []
        content.append('**\u276F Server Information**')
        content.append('Owner: {} ({})'.format(guild.owner, guild.owner.id))

        created_at = to_datetime(guild.id)
        content.append('Created: {} ({})'.format(
            humanize.naturaltime(datetime.utcnow() - created_at),
            created_at.isoformat(),
        ))

        content.append('Members: {:,}'.format(len(guild.members)))
        if guild.features:
            content.append('Features: {}'.format(', '.join(guild.features)))

        content.append('\n**\u276F Counts**')
        text_count = sum(1 for c in list(guild.channels.values())
                         if not c.is_voice)
        voice_count = len(guild.channels) - text_count
        content.append('Roles: {}'.format(len(guild.roles)))
        content.append('Text: {}'.format(text_count))
        content.append('Voice: {}'.format(voice_count))

        content.append('\n**\u276F Members**')
        status_counts = defaultdict(int)
        for member in list(guild.members.values()):
            if not member.user.presence:
                status = Status.OFFLINE
            else:
                status = member.user.presence.status
            status_counts[status] += 1

        for status, count in sorted(list(status_counts.items()),
                                    key=lambda i: str(i[0]),
                                    reverse=True):
            content.append('<{}> - {}'.format(STATUS_EMOJI[status], count))

        embed = MessageEmbed()
        if guild.icon:
            embed.set_thumbnail(url=guild.icon_url)
            # TODO: Fix whatever caused me to need to do this
            try:
                embed.color = get_dominant_colors_guild(guild)
            except:
                embed.color = 0x7289DA
        embed.description = '\n'.join(content)
        event.msg.reply('', embed=embed)

    @Plugin.command('info', '[user:user|snowflake]')
    def info(self, event, user=None):

        if not user:
            user = event.author
        else:
            if not isinstance(user, DiscoUser):
                try:
                    user = self.state.guilds[event.guild.id].members[user].user
                except KeyError:
                    try:
                        user = self.state.users[user]
                    except KeyError:
                        try:
                            user = self.bot.client.api.users_get(user)
                        except APIException:
                            return event.msg.reply(
                                'User not found :eyes:').after(3).delete()

        self.client.api.channels_typing(event.channel.id)

        content = []
        content.append('**\u276F User Information**')
        content.append('Profile: <@{}>'.format(user.id))

        created_dt = to_datetime(user.id)
        content.append('Created: {} ({})'.format(
            humanize.naturaltime(datetime.utcnow() - created_dt),
            created_dt.strftime("%b %d %Y %H:%M:%S")))

        member = event.guild.get_member(user.id) if event.guild else None

        if user.presence:  #I couldn't get this to work w/o it lol
            emoji, status = get_status_emoji(user.presence)
            content.append('Status: <{}> {}'.format(emoji, status))
            if user.presence.game and user.presence.game.name:
                if user.presence.game.type == ActivityTypes.DEFAULT:
                    content.append('{}'.format(user.presence.game.name))
                if user.presence.game.type == ActivityTypes.CUSTOM:
                    content.append('Custom Status: {}'.format(
                        user.presence.game.state))
                if user.presence.game.type == ActivityTypes.LISTENING:
                    content.append('Listening to {} on Spotify'.format(
                        user.presence.game.details)
                                   )  #In the embed, details is the songname.
                if user.presence.game.type == ActivityTypes.STREAMING:
                    content.append('Streaming: [{}]({})'.format(
                        user.presence.game.name, user.presence.game.url))

        if user.public_flags:
            badges = ''
            user_badges = list(UserFlags(user.public_flags))
            for badge in user_badges:
                badges += '<{}> '.format(BADGE_EMOJI[badge])

            content.append('Badges: {}'.format(badges))

        if member:
            content.append('\n**\u276F Member Information**')

            if member.nick:
                content.append('Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.strftime("%b %d %Y %H:%M:%S"),
            ))

            if member.roles:
                content.append('Roles: {}'.format(', '.join(
                    ('<@&{}>'.format(member.guild.roles.get(r).id)
                     for r in member.roles))))

        # Execute a bunch of queries
        newest_msg = Message.select(fn.MAX(Message.id)).where(
            (Message.author_id == user.id)
            & (Message.guild_id == event.guild.id)).tuples()[0][0]

        oldest_msg = Message.select(fn.MIN(Message.id)).where(
            (Message.author_id == user.id)
            & (Message.guild_id == event.guild.id)).tuples()[0][0]  #Slow Query

        voice = GuildVoiceSession.select(
            fn.COUNT(GuildVoiceSession.user_id),
            fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at)
        ).where((GuildVoiceSession.user_id == user.id)
                & (~(GuildVoiceSession.ended_at >> None))
                & (GuildVoiceSession.guild_id == event.guild.id)).tuples()[0]

        infractions = Infraction.select(Infraction.id).where(
            (Infraction.user_id == user.id)
            & (Infraction.guild_id == event.guild.id)).tuples()

        if newest_msg and oldest_msg:
            content.append('\n **\u276F Activity**')
            content.append('Last Message: {} ({})'.format(
                humanize.naturaltime(datetime.utcnow() -
                                     to_datetime(newest_msg)),
                to_datetime(newest_msg).strftime("%b %d %Y %H:%M:%S"),
            ))
            content.append('First Message: {} ({})'.format(
                humanize.naturaltime(datetime.utcnow() -
                                     to_datetime(oldest_msg)),
                to_datetime(oldest_msg).strftime("%b %d %Y %H:%M:%S"),
            ))

        if len(infractions) > 0:
            content.append('\n**\u276F Infractions**')
            total = len(infractions)
            content.append('Total Infractions: **{:,}**'.format(total))

        if voice[0]:
            content.append('\n**\u276F Voice**')
            content.append('Sessions: `{:,}`'.format(voice[0]))
            content.append('Time: `{}`'.format(
                str(humanize.naturaldelta(voice[1])).title()))

        embed = MessageEmbed()

        try:
            avatar = User.with_id(user.id).get_avatar_url()
        except:
            avatar = user.get_avatar_url(
            )  # This fails if the user has never been seen by speedboat.

        embed.set_author(name='{}#{} ({})'.format(
            user.username,
            user.discriminator,
            user.id,
        ),
                         icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        embed.color = get_dominant_colors_user(user, avatar)
        event.msg.reply('', embed=embed)

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))))

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder):
        message = Message.get(reminder.message_id)
        channel = self.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning(
                'Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(
            '<@{}> you asked me at {} ({}) to remind you about: {}'.format(
                message.author_id, reminder.created_at,
                humanize.naturaltime(datetime.utcnow() - reminder.created_at),
                S(reminder.content)))

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id ==
                  GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id
                 )).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            msg.delete_reaction(SNOOZE_EMOJI)
            msg.delete_reaction(GREEN_TICK_EMOJI)

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit('Ok, I\'ve snoozed that reminder for 20 minutes.')
            return

        reminder.delete_instance()

    @Plugin.command('clear', group='r', global_=True)
    def cmd_remind_clear(self, event):
        count = Reminder.delete_for_user(event.author.id)
        raise CommandSuccess('I cleared {} reminders for you'.format(count))

    @Plugin.command('add',
                    '<duration:str> <content:str...>',
                    group='r',
                    global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 15:
            raise CommandFail('You can only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() +
                        timedelta(seconds=5 * YEAR_IN_SEC)):
            raise CommandSuccess('Thats too far in the future, I\'ll forget!')

        r = Reminder.create(message_id=event.msg.id,
                            remind_at=remind_at,
                            content=content)
        self.reminder_task.set_next_schedule(r.remind_at)
        raise CommandSuccess('I\'ll remind you at {} ({})'.format(
            r.remind_at.isoformat(),
            humanize.naturaldelta(r.remind_at - datetime.utcnow()),
        ))
Exemplo n.º 14
0
    def load(self, ctx):
        super(AdminPlugin, self).load(ctx)

        self.cleans = {}
        self.inf_task = Eventual(self.clear_infractions)
        self.spawn(self.queue_infractions)
Exemplo n.º 15
0
class AdminPlugin(Plugin):
    def load(self, ctx):
        super(AdminPlugin, self).load(ctx)

        self.cleans = {}
        self.inf_task = Eventual(self.clear_infractions)
        self.spawn(self.queue_infractions)

    def queue_infractions(self):
        time.sleep(5)

        next_infraction = list(Infraction.select().where(
            (Infraction.active == 1)
            & (~(Infraction.expires_at >> None))).order_by(
                Infraction.expires_at.asc()).limit(1))

        if not next_infraction:
            self.log.info('No infractions to wait for')
            return

        self.log.info('Waiting until %s', next_infraction[0].expires_at)
        self.inf_task.set_next_schedule(next_infraction[0].expires_at)

    def clear_infractions(self):
        expired = list(Infraction.select().where((Infraction.active == 1) & (
            Infraction.expires_at < datetime.utcnow())))

        for item in expired:
            guild = self.state.guilds.get(item.guild_id)
            if not guild:
                continue

            # TODO: hacky
            type_ = {i.index: i for i in Infraction.Types.attrs}[item.type_]
            if type_ == Infraction.Types.TEMPBAN:
                # TODO: debounce
                guild.delete_ban(item.user_id)
            elif type_ == Infraction.Types.TEMPMUTE:
                member = guild.get_member(item.user_id)
                if member:
                    if item.metadata['role'] in member.roles:
                        member.remove_role(item.metadata['role'])
                else:
                    GuildMemberBackup.remove_role(item.guild_id, item.user_id,
                                                  item.metadata['role'])

            # TODO: n+1
            item.active = False
            item.save()

        self.queue_infractions()

    def restore_user(self, event, member):
        try:
            backup = GuildMemberBackup.get(guild_id=event.guild_id,
                                           user_id=member.user.id)
        except GuildMemberBackup.DoesNotExist:
            return

        kwargs = {}

        if event.config.persist.roles:
            roles = set(event.guild.roles.keys())

            if event.config.persist.role_ids:
                roles &= set(event.config.persist.role_ids)

            roles = set(backup.roles) & roles
            if roles:
                kwargs['roles'] = list(roles)

        if event.config.persist.nickname and backup.nick is not None:
            kwargs['nick'] = backup.nick

        if event.config.persist.voice and (backup.mute or backup.deaf):
            kwargs['mute'] = backup.mute
            kwargs['deaf'] = backup.deaf

        if not kwargs:
            return

        self.bot.plugins.get('ModLogPlugin').create_debounce(
            event, member.user.id, 'restore')
        member.modify(**kwargs)
        self.bot.plugins.get('ModLogPlugin').log_action_ext(
            Actions.MEMBER_RESTORE, event)

    @Plugin.listen('MessageCreate')
    def on_message_create(self, event):
        if not event.config.DONT_MENTION_B1NZY:
            return

        if B1NZY_USER_ID not in event.mentions:
            return

        member = event.guild.get_member(event.author)
        if not member or member.roles:
            return

        duration = datetime.utcnow() + timedelta(days=7)
        Infraction.tempban(self, event, member, 'AUTOBAN - mentioned b1nzy',
                           duration)
        event.message.reply(
            u'{} pinged b1nzy for some reason, they are rip now...'.format(
                member))

    @Plugin.listen('GuildMemberRemove', priority=Priority.BEFORE)
    def on_guild_member_remove(self, event):
        if event.user.id in event.guild.members:
            GuildMemberBackup.create_from_member(
                event.guild.members.get(event.user.id))

    @Plugin.listen('GuildMemberAdd')
    def on_guild_member_add(self, event):
        if not event.config.persist:
            return

        self.restore_user(event, event.member)

    @Plugin.listen('GuildMemberUpdate', priority=Priority.BEFORE)
    def on_guild_member_update(self, event):
        pre_member = event.guild.members.get(event.id)
        if not pre_member:
            return

        pre_roles = set(pre_member.roles)
        post_roles = set(event.roles)
        if pre_roles == post_roles:
            return

        removed = pre_roles - post_roles

        # If the user was unmuted, mark any temp-mutes as inactive
        if event.config.mute_role in removed:
            Infraction.clear_active(event, event.user.id,
                                    [Infraction.Types.TEMPMUTE])

    @Plugin.listen('GuildBanRemove')
    def on_guild_ban_remove(self, event):
        Infraction.clear_active(
            event, event.user.id,
            [Infraction.Types.BAN, Infraction.Types.TEMPBAN])

    @Plugin.command('unban',
                    '<user:snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    def unban(self, event, user, reason=None):
        try:
            GuildBan.get(user_id=user, guild_id=event.guild.id)
            event.guild.delete_ban(user)
        except GuildBan.DoesNotExist:
            raise CommandFail('user with id `{}` is not banned'.format(user))

        Infraction.create(guild_id=event.guild.id,
                          user_id=user,
                          actor_id=event.author.id,
                          type_=Infraction.Types.UNBAN,
                          reason=reason)
        raise CommandSuccess('unbanned user with id `{}`'.format(user))

    @Plugin.command('archive', group='infractions', level=CommandLevels.ADMIN)
    def infractions_archive(self, event):
        user = User.alias()
        actor = User.alias()

        q = Infraction.select(Infraction, user, actor).join(
            user, on=((Infraction.user_id == user.user_id).alias('user'))
        ).switch(Infraction).join(
            actor,
            on=((Infraction.actor_id == actor.user_id).alias('actor'))).where(
                Infraction.guild_id == event.guild.id)

        buff = StringIO()
        w = csv.writer(buff)

        for inf in q:
            w.writerow([
                inf.id,
                inf.user_id,
                unicode(inf.user).encode('utf-8'),
                inf.actor_id,
                unicode(inf.actor).encode('utf-8'),
                unicode({i.index: i
                         for i in Infraction.Types.attrs
                         }[inf.type_]).encode('utf-8'),
                unicode(inf.reason).encode('utf-8'),
            ])

        event.msg.reply('Ok, here is an archive of all infractions',
                        attachments=[('infractions.csv', buff.getvalue())])

    @Plugin.command('info',
                    '<infraction:int>',
                    group='infractions',
                    level=CommandLevels.MOD)
    def infraction_info(self, event, infraction):
        try:
            user = User.alias()
            actor = User.alias()

            infraction = Infraction.select(Infraction, user, actor).join(
                user, on=((Infraction.user_id == user.user_id).alias('user'))
            ).switch(Infraction).join(
                actor,
                on=((Infraction.actor_id == actor.user_id).alias('actor')
                    )).where((Infraction.id == infraction)
                             & (Infraction.guild_id == event.guild.id)).get()
        except Infraction.DoesNotExist:
            raise CommandFail(
                'cannot find an infraction with ID `{}`'.format(infraction))

        type_ = {i.index: i for i in Infraction.Types.attrs}[infraction.type_]
        embed = MessageEmbed()

        if type_ in (Infraction.Types.MUTE, Infraction.Types.TEMPMUTE):
            embed.color = 0xfdfd96
        elif type_ in (Infraction.Types.KICK, Infraction.Types.SOFTBAN):
            embed.color = 0xffb347
        else:
            embed.color = 0xff6961

        embed.title = str(type_).title()
        embed.set_thumbnail(url=infraction.user.get_avatar_url())
        embed.add_field(name='User',
                        value=unicode(infraction.user),
                        inline=True)
        embed.add_field(name='Moderator',
                        value=unicode(infraction.actor),
                        inline=True)
        embed.add_field(name='Active',
                        value='yes' if infraction.active else 'no',
                        inline=True)
        if infraction.active and infraction.expires_at:
            embed.add_field(name='Expires',
                            value=humanize.naturaldelta(infraction.expires_at -
                                                        datetime.utcnow()))
        embed.add_field(name='Reason',
                        value=infraction.reason or '_No Reason Given',
                        inline=False)
        embed.timestamp = infraction.created_at.isoformat()
        event.msg.reply('', embed=embed)

    @Plugin.command('search',
                    '[query:user|str...]',
                    group='infractions',
                    level=CommandLevels.MOD)
    def infraction_search(self, event, query=None):
        q = (Infraction.guild_id == event.guild.id)

        if query and isinstance(query, list) and isinstance(
                query[0], DiscoUser):
            query = query[0].id
        elif query:
            query = ' '.join(query)

        if query and (isinstance(query, int) or query.isdigit()):
            q &= ((Infraction.id == int(query)) |
                  (Infraction.user_id == int(query)) |
                  (Infraction.actor_id == int(query)))
        elif query:
            q &= (Infraction.reason**query)

        user = User.alias()
        actor = User.alias()

        infractions = Infraction.select(Infraction, user, actor).join(
            user, on=((Infraction.user_id == user.user_id).alias('user')
                      )).switch(Infraction).join(
                          actor,
                          on=((Infraction.actor_id == actor.user_id
                               ).alias('actor'))).where(q).order_by(
                                   Infraction.created_at.desc()).limit(6)

        tbl = MessageTable()

        tbl.set_header('ID', 'Created', 'Type', 'User', 'Moderator', 'Active',
                       'Reason')
        for inf in infractions:
            type_ = {i.index: i for i in Infraction.Types.attrs}[inf.type_]
            reason = inf.reason or ''
            if len(reason) > 256:
                reason = reason[:256] + '...'

            if inf.active:
                active = 'yes'
                if inf.expires_at:
                    active += ' (expires in {})'.format(
                        humanize.naturaldelta(inf.expires_at -
                                              datetime.utcnow()))
            else:
                active = 'no'

            tbl.add(inf.id, inf.created_at.isoformat(), str(type_),
                    unicode(inf.user), unicode(inf.actor), active, reason)

        event.msg.reply(tbl.compile())

    @Plugin.command('recent',
                    aliases=['latest'],
                    group='infractions',
                    level=CommandLevels.MOD)
    def infractions_recent(self, event):
        # TODO: f*****g write this bruh
        pass

    @Plugin.command('duration',
                    '<infraction:int> <duration:str>',
                    group='infractions',
                    level=CommandLevels.MOD)
    def infraction_duration(self, event, infraction, duration):
        try:
            inf = Infraction.get(id=infraction)
        except Infraction.DoesNotExist:
            raise CommandFail('invalid infraction (try `!infractions recent`)')

        if inf.actor_id != event.author.id and event.user_level < CommandLevels.ADMIN:
            raise CommandFail(
                'only administrators can modify the duration of infractions created by other moderators'
            )

        if not inf.active:
            raise CommandFail(
                'that infraction is not active and cannot be updated')

        expires_dt = parse_duration(duration, inf.created_at)

        converted = False
        if inf.type_ in [
                Infraction.Types.MUTE.index, Infraction.Types.BAN.index
        ]:
            inf.type_ = Infraction.Types.TEMPMUTE if inf.type_ == Infraction.Types.MUTE.index else Infraction.Types.TEMPBAN
            converted = True
        elif inf.type_ not in [
                Infraction.Types.TEMPMUTE.index, Infraction.Types.TEMPBAN.index
        ]:
            raise CommandFail(
                'cannot set the duration for that type of infraction')

        self.inf_task.set_next_schedule(expires_dt)
        inf.expires_at = expires_dt
        inf.save()

        if converted:
            raise CommandSuccess(
                'ok, I\'ve made that infraction temporary, it will now expire on {}'
                .format(inf.expires_at.isoformat()))
        else:
            raise CommandSuccess(
                'ok, I\'ve updated that infractions duration, it will now expire on {}'
                .format(inf.expires_at.isoformat()))

    @Plugin.command('reason',
                    '<infraction:int> <reason:str...>',
                    level=CommandLevels.MOD)
    def reason(self, event, infraction, reason):
        try:
            inf = Infraction.get(id=infraction)
        except Infraction.DoesNotExist:
            inf = None

        if inf is None or inf.guild_id != event.guild.id:
            event.msg.reply('Unknown infraction ID')
            return

        if not inf.actor_id:
            inf.actor_id = event.author.id

        if inf.actor_id != event.author.id and event.user_level < event.config.reason_edit_level:
            raise CommandFail(
                'you do not have the permissions required to edit other moderators infractions'
            )

        inf.reason = reason
        inf.save()

        raise CommandSuccess(
            'I\'ve updated the reason for infraction #{}'.format(inf.id))

    @Plugin.command('roles', level=CommandLevels.MOD)
    def roles(self, event):
        buff = ''
        for role in event.guild.roles.values():
            role = S(u'{} - {}\n'.format(role.id, role.name),
                     escape_codeblocks=True)
            if len(role) + len(buff) > 1990:
                event.msg.reply(u'```{}```'.format(buff))
                buff = ''
            buff += role
        return event.msg.reply(u'```{}```'.format(buff))

    @Plugin.command('restore',
                    '<user:user>',
                    level=CommandLevels.MOD,
                    group='backups')
    def restore(self, event, user):
        member = event.guild.get_member(user)
        if member:
            self.restore_user(event, member)
        else:
            raise CommandFail('invalid user')

    @Plugin.command('clear',
                    '<user_id:snowflake>',
                    level=CommandLevels.MOD,
                    group='backups')
    def backups_clear(self, event, user_id):
        deleted = bool(GuildMemberBackup.delete().where(
            (GuildMemberBackup.user_id == user_id)
            & (GuildMemberBackup.guild_id == event.guild.id)).execute())

        if deleted:
            event.msg.reply(
                ':ok_hand: I\'ve cleared the member backup for that user')
        else:
            raise CommandFail(
                'I couldn\t find any member backups for that user')

    def can_act_on(self, event, victim_id, throw=True):
        if event.author.id == victim_id:
            if not throw:
                return False
            raise CommandFail('cannot execute that action on yourself')

        victim_level = self.bot.plugins.get('CorePlugin').get_level(
            event.guild, victim_id)

        if event.user_level <= victim_level:
            if not throw:
                return False
            raise CommandFail('invalid permissions')

        return True

    @Plugin.command('mute',
                    '<user:user|snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    def mute(self, event, user, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            if not event.config.mute_role:
                raise CommandFail('mute is not setup on this server')

            existed = False
            # If the user is already muted check if we can take this from a temp
            #  to perma mute.
            if event.config.mute_role in member.roles:
                existed = Infraction.clear_active(event, member.id,
                                                  [Infraction.Types.TEMPMUTE])

                # The user is 100% muted and not tempmuted at this point, so lets bail
                if not existed:
                    raise CommandFail(u'{} is already muted'.format(
                        member.user))

            Infraction.mute(self, event, member, reason)

            if event.config.confirm_actions:
                existed = u' [was temp-muted]' if existed else ''
                event.msg.reply(
                    maybe_string(
                        reason,
                        u':ok_hand: {u} is now muted (`{o}`)' + existed,
                        u':ok_hand: {u} is now muted' + existed,
                        u=member.user,
                    ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('tempmute',
                    '<user:user|snowflake> <duration:str> [reason:str...]',
                    level=CommandLevels.MOD)
    def tempmute(self, event, user, duration, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            if not event.config.mute_role:
                raise CommandFail('mute is not setup on this server')

            if event.config.mute_role in member.roles:
                raise CommandFail(u'{} is already muted'.format(member.user))

            expire_dt = parse_duration(duration)

            # Reset the infraction task so we make sure it runs after this new infraction
            self.inf_task.set_next_schedule(expire_dt)

            # Create the infraction
            Infraction.tempmute(self, event, member, reason, expire_dt)

            if event.config.confirm_actions:
                event.msg.reply(
                    maybe_string(
                        reason,
                        u':ok_hand: {u} is now muted for {t} (`{o}`)',
                        u':ok_hand: {u} is now muted for {t}',
                        u=member.user,
                        t=humanize.naturaldelta(expire_dt - datetime.utcnow()),
                    ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('unmute', '<user:user|snowflake>', level=CommandLevels.MOD)
    def unmute(self, event, user, reason=None):
        # TOOD: eventually we should pull the role from the GuildMemberBackup if they arent in server
        member = event.guild.get_member(user)

        if member:
            self.can_act_on(event, member.id)
            if not event.config.mute_role:
                raise CommandFail('mute is not setup on this server')

            if event.config.mute_role not in member.roles:
                raise CommandFail(u'{} is not muted'.format(member.user))

            Infraction.clear_active(
                event, member.id,
                [Infraction.Types.MUTE, Infraction.Types.TEMPMUTE])

            self.bot.plugins.get('ModLogPlugin').create_debounce(
                event,
                member.user.id,
                'unmuted',
                actor=unicode(event.author),
                roles=[event.config.mute_role])

            member.remove_role(event.config.mute_role)

            if event.config.confirm_actions:
                event.msg.reply(u':ok_hand: {} is now unmuted'.format(
                    member.user))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('kick',
                    '<user:user|snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    def kick(self, event, user, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            Infraction.kick(self, event, member, reason)
            if event.config.confirm_actions:
                event.msg.reply(
                    maybe_string(
                        reason,
                        u':ok_hand: kicked {u} (`{o}`)',
                        u':ok_hand: kicked {u}',
                        u=member.user,
                    ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('mkick', parser=True, level=CommandLevels.MOD)
    @Plugin.parser.add_argument('users', type=long, nargs='+')
    @Plugin.parser.add_argument('-r',
                                '--reason',
                                default='',
                                help='reason for modlog')
    def mkick(self, event, args):
        members = []
        for user_id in args.users:
            member = event.guild.get_member(user_id)
            if not member:
                # TODO: this sucks, batch these
                raise CommandFail(
                    'failed to kick {}, user not found'.format(user_id))

            if not self.can_act_on(event, member.id, throw=False):
                raise CommandFail(
                    'failed to kick {}, invalid permissions'.format(user_id))

            members.append(member)

        msg = event.msg.reply('Ok, kick {} users for `{}`?'.format(
            len(members), args.reason or 'no reason'))
        msg.chain(False).\
            add_reaction(GREEN_TICK_EMOJI).\
            add_reaction(RED_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.
                 user_id == event.author.id)).get(timeout=10)
        except gevent.Timeout:
            return
        finally:
            msg.delete()

        if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
            return

        for member in members:
            Infraction.kick(self, event, member, args.reason)

        raise CommandSuccess('kicked {} users'.format(len(members)))

    @Plugin.command('ban',
                    '<user:user|snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    @Plugin.command('forceban',
                    '<user:snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    def ban(self, event, user, reason=None):
        member = None

        if isinstance(user, (int, long)):
            self.can_act_on(event, user)
            Infraction.ban(self, event, user, reason, guild=event.guild)
        else:
            member = event.guild.get_member(user)
            if member:
                self.can_act_on(event, member.id)
                Infraction.ban(self, event, member, reason, guild=event.guild)
            else:
                raise CommandFail('invalid user')

        if event.config.confirm_actions:
            event.msg.reply(
                maybe_string(
                    reason,
                    u':ok_hand: banned {u} (`{o}`)',
                    u':ok_hand: banned {u}',
                    u=member.user if member else user,
                ))

    @Plugin.command('softban',
                    '<user:user|snowflake> [reason:str...]',
                    level=CommandLevels.MOD)
    def softban(self, event, user, reason=None):
        """
        Ban then unban a user from the server (with an optional reason for the modlog)
        """
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            Infraction.softban(self, event, member, reason)
            if event.config.confirm_actions:
                event.msg.reply(
                    maybe_string(
                        reason,
                        u':ok_hand: soft-banned {u} (`{o}`)',
                        u':ok_hand: soft-banned {u}',
                        u=member.user,
                    ))
        else:
            raise CommandFail('invald user')

    @Plugin.command('tempban',
                    '<user:user|snowflake> <duration:str> [reason:str...]',
                    level=CommandLevels.MOD)
    def tempban(self, event, duration, user, reason=None):
        member = event.guild.get_member(user)
        if member:
            self.can_act_on(event, member.id)
            expires_dt = parse_duration(duration)
            self.inf_task.set_next_schedule(expires_dt)
            Infraction.tempban(self, event, member, reason, expires_dt)
            if event.config.confirm_actions:
                event.msg.reply(
                    maybe_string(
                        reason,
                        u':ok_hand: temp-banned {u} for {t} (`{o}`)',
                        u':ok_hand: temp-banned {u} for {t}',
                        u=member.user,
                        t=humanize.naturaldelta(expires_dt -
                                                datetime.utcnow()),
                    ))
        else:
            raise CommandFail('invalid user')

    @Plugin.command('archive here',
                    '[size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'all'})
    @Plugin.command('archive all',
                    '[size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'all'})
    @Plugin.command('archive user',
                    '<user:user|snowflake> [size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'user'})
    @Plugin.command('archive channel',
                    '<channel:channel|snowflake> [size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'channel'})
    def archive(self, event, size=50, mode=None, user=None, channel=None):
        if 0 > size >= 15000:
            raise CommandFail('too many messages must be between 1-15000')

        q = Message.select(Message.id).join(User).order_by(
            Message.id.desc()).limit(size)

        if mode in ('all', 'channel'):
            q = q.where((Message.channel_id == (channel or event.channel).id))
        else:
            q = q.where((Message.author_id ==
                         (user if isinstance(user, (int, long)) else user.id))
                        & (Message.guild_id == event.guild.id))

        archive = MessageArchive.create_from_message_ids([i.id for i in q])
        event.msg.reply('OK, archived {} messages at {}'.format(
            len(archive.message_ids), archive.url))

    @Plugin.command('clean cancel', level=CommandLevels.MOD)
    def clean_cacnel(self, event):
        if event.channel.id not in self.cleans:
            raise CommandFail('no clean is running in this channel')

        self.cleans[event.channel.id].kill()
        event.msg.reply('Ok, the running clean was cancelled')

    @Plugin.command('clean all',
                    '[size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'all'})
    @Plugin.command('clean bots',
                    '[size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'bots'})
    @Plugin.command('clean user',
                    '<user:user> [size:int]',
                    level=CommandLevels.MOD,
                    context={'mode': 'user'})
    def clean(self, event, user=None, size=25, typ=None, mode='all'):
        """
        Removes messages
        """
        if 0 > size >= 10000:
            raise CommandFail('too many messages must be between 1-10000')

        if event.channel.id in self.cleans:
            raise CommandFail('a clean is already running on this channel')

        query = Message.select(Message.id).where(
            (Message.deleted >> False)
            & (Message.channel_id == event.channel.id)
            & (Message.timestamp >
               (datetime.utcnow() - timedelta(days=13)))).join(User).order_by(
                   Message.timestamp.desc()).limit(size)

        if mode == 'bots':
            query = query.where((User.bot >> True))
        elif mode == 'user':
            query = query.where((User.user_id == user.id))

        messages = [i[0] for i in query.tuples()]

        if len(messages) > 100:
            msg = event.msg.reply(
                'Woah there, that will delete a total of {} messages, please confirm.'
                .format(len(messages)))

            msg.chain(False).\
                add_reaction(GREEN_TICK_EMOJI).\
                add_reaction(RED_TICK_EMOJI)

            try:
                mra_event = self.wait_for_event(
                    'MessageReactionAdd',
                    message_id=msg.id,
                    conditional=lambda e:
                    (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and
                     e.user_id == event.author.id)).get(timeout=10)
            except gevent.Timeout:
                return
            finally:
                msg.delete()

            if mra_event.emoji.id != GREEN_TICK_EMOJI_ID:
                return

            event.msg.reply(
                ':wastebasket: Ok please hold on while I delete those messages...'
            ).after(5).delete()

        def run_clean():
            for chunk in chunks(messages, 100):
                self.client.api.channels_messages_delete_bulk(
                    event.channel.id, chunk)

        self.cleans[event.channel.id] = gevent.spawn(run_clean)
        self.cleans[event.channel.id].join()
        del self.cleans[event.channel.id]

    @Plugin.command('add',
                    '<user:user> <role:str> [reason:str...]',
                    level=CommandLevels.MOD,
                    context={'mode': 'add'},
                    group='role')
    @Plugin.command('rmv',
                    '<user:user> <role:str> [reason:str...]',
                    level=CommandLevels.MOD,
                    context={'mode': 'remove'},
                    group='role')
    @Plugin.command('remove',
                    '<user:user> <role:str> [reason:str...]',
                    level=CommandLevels.MOD,
                    context={'mode': 'remove'},
                    group='role')
    def role_add(self, event, user, role, reason=None, mode=None):
        role_obj = None

        if role.isdigit() and int(role) in event.guild.roles.keys():
            role_obj = event.guild.roles[int(role)]
        else:
            # First try exact match
            exact_matches = [
                i for i in event.guild.roles.values()
                if i.name.lower().replace(' ', '') == role.lower()
            ]
            if len(exact_matches) == 1:
                role_obj = exact_matches[0]
            else:
                # Otherwise we fuzz it up
                rated = sorted(
                    [(fuzz.partial_ratio(role, r.name.replace(' ', '')), r)
                     for r in event.guild.roles.values()],
                    key=lambda i: i[0],
                    reverse=True)

                if rated[0][0] > 40:
                    if len(rated) == 1:
                        role_obj = rated[0][1]
                    elif rated[0][0] - rated[1][0] > 20:
                        role_obj = rated[0][1]

        if not role_obj:
            raise CommandFail(
                'too many matches for that role, try something more exact or the role ID'
            )

        author_member = event.guild.get_member(event.author)
        highest_role = sorted(
            [event.guild.roles.get(r) for r in author_member.roles],
            key=lambda i: i.position,
            reverse=True)
        if not author_member.owner and (
                not highest_role
                or highest_role[0].position < role_obj.position):
            raise CommandFail(
                'you can only {} roles that are ranked lower than your highest role'
                .format(mode))

        member = event.guild.get_member(user)
        if not member:
            raise CommandFail('invalid member')

        self.can_act_on(event, member.id)

        if mode == 'add' and role_obj.id in member.roles:
            raise CommandFail(u'{} already has the {} role'.format(
                member, role_obj.name))
        elif mode == 'remove' and role_obj.id not in member.roles:
            return CommandFail(u'{} doesn\'t have the {} role'.format(
                member, role_obj.name))

        self.bot.plugins.get('ModLogPlugin').create_debounce(
            event,
            member.user.id,
            mode + '_role',
            actor=event.author,
            reason=reason or 'no reason')

        if mode == 'add':
            member.add_role(role_obj.id)
        else:
            member.remove_role(role_obj.id)

        event.msg.reply(u':ok_hand: {} role {} to {}'.format(
            'added' if mode == 'add' else 'removed', role_obj.name, member))

    @Plugin.command('stats', '<user:user>', level=CommandLevels.MOD)
    def msgstats(self, event, user):
        # Query for the basic aggregate message statistics
        q = list(
            Message.select(
                fn.Count('*'),
                fn.Sum(fn.char_length(Message.content)),
                fn.Sum(fn.array_length(Message.emojis, 1)),
                fn.Sum(fn.array_length(Message.mentions, 1)),
                fn.Sum(fn.array_length(Message.attachments, 1)),
            ).where((Message.author_id == user.id)).tuples())[0]

        reactions_given = list(
            Reaction.select(
                fn.Count('*'),
                Reaction.emoji_id,
                Reaction.emoji_name,
            ).join(Message, on=(Message.id == Reaction.message_id)).where(
                (Reaction.user_id == user.id)).group_by(
                    Reaction.emoji_id, Reaction.emoji_name).order_by(
                        fn.Count('*').desc()).tuples())

        # Query for most used emoji
        emojis = list(
            Message.raw(
                '''
            SELECT gm.emoji_id, gm.name, count(*)
            FROM (
                SELECT unnest(emojis) as id
                FROM messages
                WHERE author_id=%s
            ) q
            JOIN guildemojis gm ON gm.emoji_id=q.id
            GROUP BY 1, 2
            ORDER BY 3 DESC
            LIMIT 1
        ''', (user.id, )).tuples())

        deleted = Message.select().where((Message.author_id == user.id)
                                         & (Message.deleted == 1)).count()

        embed = MessageEmbed()
        embed.fields.append(
            MessageEmbedField(name='Total Messages Sent',
                              value=q[0] or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Characters Sent',
                              value=q[1] or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Deleted Messages',
                              value=deleted or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Custom Emoji\'s',
                              value=q[2] or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Mentions',
                              value=q[3] or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Attachments',
                              value=q[4] or '0',
                              inline=True))
        embed.fields.append(
            MessageEmbedField(name='Total Reactions',
                              value=sum(i[0] for i in reactions_given),
                              inline=True))

        if reactions_given:
            emoji = reactions_given[0][
                2] if not reactions_given[0][1] else '<:{}:{}>'.format(
                    reactions_given[0][2], reactions_given[0][1])
            embed.fields.append(
                MessageEmbedField(name='Most Used Reaction',
                                  value=u'{} (used {} times)'.format(
                                      emoji,
                                      reactions_given[0][0],
                                  ),
                                  inline=True))

        if emojis:
            embed.fields.append(
                MessageEmbedField(
                    name='Most Used Emoji',
                    value=u'<:{1}:{0}> (`{1}`, used {2} times)'.format(
                        *emojis[0])))

        embed.thumbnail = MessageEmbedThumbnail(url=user.avatar_url)
        embed.color = get_dominant_colors_user(user)
        event.msg.reply('', embed=embed)

    @Plugin.command('emojistats',
                    '<mode:str> <sort:str>',
                    level=CommandLevels.MOD)
    def emojistats_custom(self, event, mode, sort):
        if mode not in ('server', 'global'):
            raise CommandFail(
                'invalid emoji mode, must be `server` or `global`')

        if sort not in ('least', 'most'):
            raise CommandFail('invalid emoji sort, must be `least` or `most`')

        order = 'DESC' if sort == 'most' else 'ASC'

        if mode == 'server':
            q = CUSTOM_EMOJI_STATS_SERVER_SQL.format(order,
                                                     guild=event.guild.id)
        else:
            q = CUSTOM_EMOJI_STATS_GLOBAL_SQL.format(order,
                                                     guild=event.guild.id)

        q = list(GuildEmoji.raw(q).tuples())

        tbl = MessageTable()
        tbl.set_header('Count', 'Name', 'ID')
        for emoji_id, name, count in q:
            tbl.add(count, name, emoji_id)

        event.msg.reply(tbl.compile())

    @Plugin.command('prune',
                    '[uses:int]',
                    level=CommandLevels.ADMIN,
                    group='invites')
    def invites_prune(self, event, uses=1):
        invites = [
            i for i in event.guild.get_invites()
            if i.uses <= uses and i.created_at < (datetime.utcnow() -
                                                  timedelta(hours=1))
        ]

        if not invites:
            return event.msg.reply(
                'I didn\'t find any invites matching your criteria')

        msg = event.msg.reply(
            'Ok, a total of {} invites created by {} users with {} total uses would be pruned.'
            .format(len(invites), len({i.inviter.id
                                       for i in invites}),
                    sum(i.uses for i in invites)))

        msg.chain(False).\
            add_reaction(GREEN_TICK_EMOJI).\
            add_reaction(RED_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e:
                (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.
                 user_id == event.author.id)).get(timeout=10)
        except gevent.Timeout:
            msg.reply('Not executing invite prune')
            msg.delete()
            return

        msg.delete()

        if mra_event.emoji.id == GREEN_TICK_EMOJI_ID:
            msg = msg.reply('Pruning invites...')
            for invite in invites:
                invite.delete()
            msg.edit('Ok, invite prune completed')
        else:
            msg = msg.reply('Not pruning invites')

    @Plugin.command('clean',
                    '<user:user|snowflake> [count:int] [emoji:str]',
                    level=CommandLevels.MOD,
                    group='reactions')
    def reactions_clean(self, event, user, count=10, emoji=None):
        if isinstance(user, DiscoUser):
            user = user.id

        if count > 50:
            raise CommandFail('cannot clean more than 50 reactions')

        lock = rdb.lock('clean-reactions-{}'.format(user))
        if not lock.acquire(blocking=False):
            raise CommandFail('already running a clean on user')

        query = [
            (Reaction.user_id == user),
            (Message.guild_id == event.guild.id),
            (Message.deleted == 0),
        ]

        if emoji:
            emoji_id = EMOJI_RE.findall(emoji)
            if emoji_id:
                query.append((Reaction.emoji_id == emoji_id[0]))
            else:
                # TODO: validation?
                query.append((Reaction.emoji_name == emoji))

        try:
            reactions = list(
                Reaction.select(
                    Reaction.message_id,
                    Reaction.emoji_id,
                    Reaction.emoji_name,
                    Message.channel_id,
                ).join(
                    Message,
                    on=(Message.id == Reaction.message_id),
                ).where(reduce(operator.and_, query)).order_by(
                    Reaction.message_id.desc()).limit(count).tuples())

            if not reactions:
                raise CommandFail('no reactions to purge')

            msg = event.msg.reply('Hold on while I clean {} reactions'.format(
                len(reactions)))

            for message_id, emoji_id, emoji_name, channel_id in reactions:
                if emoji_id:
                    emoji = '{}:{}'.format(emoji_name, emoji_id)
                else:
                    emoji = emoji_name

                self.client.api.channels_messages_reactions_delete(
                    channel_id, message_id, emoji, user)

            msg.edit('Ok, I cleaned {} reactions'.format(len(reactions), ))
        finally:
            lock.release()

    @Plugin.command('log',
                    '<user:user|snowflake>',
                    group='voice',
                    level=CommandLevels.MOD)
    def voice_log(self, event, user):
        if isinstance(user, DiscoUser):
            user = user.id

        sessions = GuildVoiceSession.select(
            GuildVoiceSession.user_id, GuildVoiceSession.channel_id,
            GuildVoiceSession.started_at, GuildVoiceSession.ended_at).where(
                (GuildVoiceSession.user_id == user)
                & (GuildVoiceSession.guild_id == event.guild.id)).order_by(
                    GuildVoiceSession.started_at.desc()).limit(10)

        tbl = MessageTable()
        tbl.set_header('Channel', 'Joined At', 'Duration')

        for session in sessions:
            tbl.add(
                unicode(
                    self.state.channels.get(session.channel_id) or 'UNKNOWN'),
                '{} ({} ago)'.format(
                    session.started_at.isoformat(),
                    humanize.naturaldelta(datetime.utcnow() -
                                          session.started_at)),
                humanize.naturaldelta(session.ended_at - session.started_at)
                if session.ended_at else 'Active')

        event.msg.reply(tbl.compile())
Exemplo n.º 16
0
class UtilitiesPlugin(Plugin):
    def load(self, ctx):
        super(UtilitiesPlugin, self).load(ctx)
        self.reminder_task = Eventual(self.trigger_reminders)
        self.spawn_later(10, self.queue_reminders)
        self.server_owners = {
            '166304313004523520': "airplane, PokeBlobs", # OGNovuh#0003
            '175805198017626114': "Swagger's Hangout", # SwaggerSouls#4295
            '127060170802069505': "Hotel Fitz", # Fitz#9588
            '107941250228830208': "Jameskii", # Jameskii#0001
            '197316087879172096': "Daniel's Discord", # danielharrison#0001
            '226912511302041601': "xd Official Discord", # McCreamy#6793
            '265986744019714050': "Dooocord", # TheDooo#4877
            '202271067912404992': "Donut Operator's Discord", # DonutOperator#5512
        }
        self.server_managers = {
            '158200017906171904': "Hotel Fitz", # Xentiran#0007
            '191793155685744640': "Hotel Fitz, Jameskii, Swagger's Hangout", # Terminator966#0966
            '227888061353164810': "Hotel Fitz", # emily#2900
            '166304313004523520': "Jameskii", # OGNovuh#0003
            '324645959265157120': "Swagger's Hangout", # Mushy The Wizard#2319
            '285238487618420737': "Swagger's Hangout", # Boubie#0305
            '244907038667177984': "Swagger's Hangout", # Kata#7886
            '194903108495605760': "Swagger's Hangout", # Solaire#4156
            '138842652585099264': "Swagger's Hangout", # Xeano#4444
            '285712318464000000': "xd Official Discord", # Nuna#0001
            '340753167098839051': "Donut Operator's Discord", # Valkyrie#5555
            '339254723091890177': "Donut Operator's Discord", # AmazonPrimes#7409
        }

    def queue_reminders(self):
        try:
            next_reminder = Reminder.select().order_by(
                Reminder.remind_at.asc()
            ).limit(1).get()
        except Reminder.DoesNotExist:
            return

        self.reminder_task.set_next_schedule(next_reminder.remind_at)

    @Plugin.command('coin', group='random', global_=True)
    def coin(self, event):
        """
        Flip a coin
        """
        event.msg.reply(random.choice(['heads', 'tails']))

    @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True)
    def random_number(self, event, end=10, start=0):
        """
        Returns a random number
        """

        # Because someone will be an idiot
        if end > 9223372036854775807:
            return event.msg.reply(':warning: ending number too big!')

        if end <= start:
            return event.msg.reply(':warning: ending number must be larger than starting number!')

        event.msg.reply(str(random.randint(start, end)))

    @Plugin.command('cat', global_=True)
    def cat(self, event):
        # Sometimes random.cat gives us gifs (smh)
        for _ in range(3):
            try:
                r = requests.get('http://aws.random.cat/meow')
                r.raise_for_status()
            except:
                continue

            url = r.json()['file']
            if not url.endswith('.gif'):
                break
        else:
            return event.msg.reply('404 cat not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('cat.jpg', r.content)])

    @Plugin.command('duck', aliases=['quack'], global_=True)
    def duck(self, event):
        # Sometimes random.cat gives us gifs (smh)
        for _ in range(3):
            try:
                r = requests.get('https://random-d.uk/api/v1/random?type=jpg')
                r.raise_for_status()
            except:
                continue

            url = r.json()['url']
            if not url.endswith('.gif'):
                break
        else:
            return event.msg.reply('404 duck not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('duck.jpg', r.content)])
    
    @Plugin.command('bunny', aliases=['bunbun', 'wabbit', 'fluff'], global_=True)
    def bunny(self, event):
        try:
            r = requests.get('https://api.bunnies.io/v2/loop/random/?media=png,gif')
            r.raise_for_status()
        except:
            return event.msg.reply('404 bunny not found :(')

        media = r.json()['media']
        ext = 'png'
        if (media['gif']):
            url = media['gif']
            ext='gif'
        elif(media['png']):
            url = media['png']
        else:
            return event.msg.reply('404 bunny not found :(')

        r = requests.get(url)
        try:
            r.raise_for_status()
        except requests.HTTPError as e:
            self.log.error('Bunny fetch failed: {}'.format(str(e)))
            return event.msg.reply('404 bunny not found :(')

        try:
            event.msg.reply('', attachments=[('bunny.{}'.format(ext), r.content)])
        except APIException:
            self.bunny(event)
    
    @Plugin.command('dog', global_=True)
    def dog(self, event):
        # Sometimes random.dog gives us gifs or mp4s (smh)
        for _ in range(3):
            try:
                r = requests.get('https://random.dog/woof.json')
                r.raise_for_status()
            except:
                continue
            
            url = r.json()['url']
            if not url.endswith('.gif') or not url.endswith('.mp4'):
                break
        else:
            return event.msg.reply('404 dog not found :(')

        r = requests.get(url)
        r.raise_for_status()
        event.msg.reply('', attachments=[('dog.jpg', r.content)])

    @Plugin.command('bird', aliases=['birb'], global_=True)
    def bird(self, event):
        # Sometimes random.birb gives us gifs or mp4s (smh)
        for _ in range(3):
            try:
                r = requests.get('https://random.birb.pw/tweet.json/')
                r.raise_for_status()
            except:
                continue
            
            name = r.json()['file']
            if not name.endswith('.gif') or not name.endswith('.mp4'):
                break
        else:
            return event.msg.reply('404 bird not found :(')

        r = requests.get('https://random.birb.pw/img/' + name)
        r.raise_for_status()
        event.msg.reply('', attachments=[('bird.jpg', r.content)])

    @Plugin.command('emoji', '<emoji:str>', global_=True)
    def emoji(self, event, emoji):
        if not EMOJI_RE.match(emoji):
            return event.msg.reply(u'Unknown emoji: `{}`'.format(S(emoji)))

        fields = []

        name, eid = EMOJI_RE.findall(emoji)[0]
        fields.append('**ID:** {}'.format(eid))
        fields.append('**Name:** {}'.format(S(name)))

        guild = self.state.guilds.find_one(lambda v: eid in v.emojis)
        if guild:
            fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id))

        anim = emoji.startswith('<a:')
        fields.append('**Animated:** {}'.format('Yes' if anim else 'No'))

        ext = 'gif' if anim else 'png'
        url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext)
        r = requests.get(url)
        r.raise_for_status()
        return event.msg.reply('\n'.join(fields), attachments=[('emoji.'+ext, r.content)])

    @Plugin.command('jumbo', '<emojis:str...>', global_=True)
    def jumbo(self, event, emojis):
        urls = []
        first_emote = emojis.split(' ')[0]
        if first_emote.startswith('<a:'):
            if EMOJI_RE.match(first_emote):
                eid = EMOJI_RE.findall(first_emote)[0]
                url = 'https://discordapp.com/api/emojis/{}.gif'.format(eid[1])
                r = requests.get(url)
                r.raise_for_status()
                return event.msg.reply('', attachments=[('emoji.gif', r.content)])
            else:
                raise CommandFail('Invalid Animated Emote.')
        else:
            for emoji in emojis.split(' ')[:5]:
                if emoji.startswith('<a:'):
                    continue
                if EMOJI_RE.match(emoji):
                    _, eid = EMOJI_RE.findall(emoji)[0]
                    urls.append('https://discordapp.com/api/emojis/{}.png'.format(eid))
                else:
                    urls.append(get_emoji_url(emoji))

            width, height, images = 0, 0, []

            for r in Pool(6).imap(requests.get, urls):
                try:
                    r.raise_for_status()
                except requests.HTTPError:
                    return

                img = Image.open(BytesIO(r.content))
                height = img.height if img.height > height else height
                width += img.width + 10
                images.append(img)

            image = Image.new('RGBA', (width, height))
            width_offset = 0
            for img in images:
                image.paste(img, (width_offset, 0))
                width_offset += img.width + 10

            combined = BytesIO()
            image.save(combined, 'png', quality=55)
            combined.seek(0)
            return event.msg.reply('', attachments=[('emoji.png', combined)])

    @Plugin.command('ping', level=CommandLevels.ADMIN)
    def command_ping(self, event):
        return event.msg.reply(':ping_pong: stop pinging me asshole!!!!!')
    
    @Plugin.command('seen', '<user:user>', global_=True)
    def seen(self, event, user):
        try:
            msg = Message.select(Message.timestamp).where(
                Message.author_id == user.id
            ).order_by(Message.timestamp.desc()).limit(1).get()
        except Message.DoesNotExist:
            return event.msg.reply(u"I've never seen {}".format(user))

        event.msg.reply(u'I last saw {} {} ago (at {})'.format(
            user,
            humanize.naturaldelta(datetime.utcnow() - msg.timestamp),
            msg.timestamp
        ))

    @Plugin.command('search', '<query:str...>', global_=True)
    def search(self, event, query):
        queries = []

        if query.isdigit():
            queries.append((User.user_id == query))

        q = USER_MENTION_RE.findall(query)
        if len(q) and q[0].isdigit():
            queries.append((User.user_id == q[0]))
        else:
            queries.append((User.username ** u'%{}%'.format(query.replace('%', ''))))

        if '#' in query:
            username, discrim = query.rsplit('#', 1)
            if discrim.isdigit():
                queries.append((
                    (User.username == username) &
                    (User.discriminator == int(discrim))))

        users = User.select().where(reduce(operator.or_, queries))
        if len(users) == 0:
            return event.msg.reply(u'No users found for query `{}`'.format(S(query, escape_codeblocks=True)))

        if len(users) == 1:
            if users[0].user_id in self.state.users:
                return self.info(event, self.state.users.get(users[0].user_id))

        return event.msg.reply(u'Found the following users for your query: ```{}```'.format(
            u'\n'.join(map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25]))
        ))

    @Plugin.command('server', '[guild_id:snowflake]', global_=True)
    def server(self, event, guild_id=None):
        guild = self.state.guilds.get(guild_id) if guild_id else event.guild
        if not guild:
            raise CommandFail('invalid server')

        content = []
        content.append(u'**\u276F Server Information**')

        created_at = to_datetime(guild.id)
        content.append(u'Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_at),
            created_at.isoformat(),
        ))
        content.append(u'Members: {:,d}'.format(len(guild.members)))
        content.append(u'Features: {}'.format(', '.join(guild.features) or 'none'))

        content.append(u'\n**\u276F Counts**')
        text_count = sum(1 for c in guild.channels.values() if not c.is_voice)
        voice_count = len(guild.channels) - text_count
        content.append(u'Roles: {}'.format(len(guild.roles)))
        content.append(u'Text: {}'.format(text_count))
        content.append(u'Voice: {}'.format(voice_count))

        content.append(u'\n**\u276F Members**')
        status_counts = defaultdict(int)
        for member in guild.members.values():
            if not member.user.presence:
                status = Status.OFFLINE
            else:
                status = member.user.presence.status
            status_counts[status] += 1

        for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True):
            content.append(u'<{}> - {}'.format(
                STATUS_EMOJI[status], format(count, ',d')
            ))

        embed = MessageEmbed()
        if guild.icon:
            embed.set_thumbnail(url=guild.icon_url)
            embed.color = get_dominant_colors_guild(guild)
        embed.description = '\n'.join(content)
        event.msg.reply('', embed=embed)

    # --------------Coded by Xenthys#0001 for Rawgoat--------------

    def fetch_user(self, id, raise_on_error=True):
        try:
            r = self.bot.client.api.http(Routes.USERS_GET, dict(user=id))
            return DiscoUser.create(self.bot.client.api.client,r.json())
        except APIException:
            if raise_on_error:
                raise CommandFail('unknown user')
            return

    @Plugin.command('info', '[user:user|snowflake]')
    def info(self, event, user=None):
        if user is None:
            user = event.author

        user_id = 0
        if isinstance(user, (int, long)):
            user_id = user
            user = self.state.users.get(user)

        if user and not user_id:
            user = self.state.users.get(user.id)

        if not user:
            if user_id:
                user = self.fetch_user(user_id)
                User.from_disco_user(user)
            else:
                raise CommandFail('unknown user')

        content = []
        content.append(u'**\u276F User Information**')
        content.append(u'ID: {}'.format(user.id))
        content.append(u'Profile: <@{}>'.format(user.id))

        if user.presence:
            emoji, status = get_status_emoji(user.presence)
            content.append('Status: {} <{}>'.format(status, emoji))

            game = user.presence.game
            if game and game.name:
                activity = ['Playing', 'Stream'][int(game.type)] if game.type < 2 else None
                if not game.type:
                    if game.name == 'Spotify':
                        activity = 'Listening to'
                    else:
                        activity = None
                if activity:
                    content.append(u'{}: {}'.format(activity,
                        u'[{}]({})'.format(game.name, game.url) if game.url else game.name
                    ))


        created_dt = to_datetime(user.id)
        content.append('Created: {} ago ({})'.format(
            humanize.naturaldelta(datetime.utcnow() - created_dt),
            created_dt.isoformat()
        ))

        for i in self.server_owners:
            if i == str(user.id):
                content.append('Server Ownership: {}'.format(self.server_owners[i]))
        
        for i in self.server_managers:
            if i == str(user.id):
                content.append('Community Manager: {}'.format(self.server_managers[i]))

        if user.id == self.state.me.id:
            content.append('Documentation: https://aetherya.stream/')
        elif rdb.sismember('global_admins', user.id):
            content.append('Airplane Staff: Global Administrator')
        elif rdb.sismember('server_managers', user.id):
            content.append('Server Manager')
        elif rdb.sismember('server_owner', user.id):
            content.append('Server Owner')

        member = event.guild.get_member(user.id) if event.guild else None
        if member:
            content.append(u'\n**\u276F Member Information**')

            if member.nick:
                content.append(u'Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.isoformat(),
            ))

            if member.roles:
                roles = []
                for r in member.roles:
                    roles.append(member.guild.roles.get(r))
                roles = sorted(roles, key=lambda r: r.position, reverse=True)
                total = len(member.roles)
                roles = roles[:20]
                content.append(u'Roles ({}): {}{}'.format(
                    total, ' '.join(r.mention for r in roles),
                    ' (+{})'.format(total-20) if total > 20 else ''
                ))

        # Execute a bunch of queries async
        newest_msg = Message.select(Message.timestamp).where(
            (Message.author_id == user.id) &
            (Message.guild_id == event.guild.id)
        ).order_by(Message.timestamp.desc()).limit(1).async()

        # oldest_msg = Message.select(Message.timestamp).where(
        #     (Message.author_id == user.id) &
        #     (Message.guild_id == event.guild.id)
        # ).order_by(Message.timestamp.asc()).limit(1).async()

        infractions = Infraction.select(
            Infraction.guild_id,
            fn.COUNT('*')
        ).where(
            (Infraction.user_id == user.id) &
            (Infraction.type_ != 6) & # Unban
            (~(Infraction.reason ** '[NOTE]%'))
        ).group_by(Infraction.guild_id).tuples().async()

        voice = GuildVoiceSession.select(
            GuildVoiceSession.user_id,
            fn.COUNT('*'),
            fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at)
        ).where(
            (GuildVoiceSession.user_id == user.id) &
            (~(GuildVoiceSession.ended_at >> None))
        ).group_by(GuildVoiceSession.user_id).tuples().async()

        # Wait for them all to complete (we're still going to be as slow as the
        #  slowest query, so no need to be smart about this.)
        try:
            wait_many(newest_msg, infractions, voice, timeout=3)
        except gevent.Timeout:
            pass
        tags = to_tags(guild_id=event.msg.guild.id)
            
        if newest_msg.value:
            content.append(u'\n **\u276F Activity**')
            statsd.timing('sql.duration.newest_msg', newest_msg.value._query_time, tags=tags)
            newest_msg = newest_msg.value.get()
            content.append('Last Message: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - newest_msg.timestamp),
                newest_msg.timestamp.isoformat(),
            ))

        # if oldest_msg.value:
        #     statsd.timing('sql.duration.oldest_msg', oldest_msg.value._query_time, tags=tags)
        #     oldest_msg = oldest_msg.value.get()
        #     content.append('First Message: {} ago ({})'.format(
        #         humanize.naturaldelta(datetime.utcnow() - oldest_msg.timestamp),
        #         oldest_msg.timestamp.isoformat(),
        #     ))

        if infractions.value:
            statsd.timing('sql.duration.infractions', infractions.value._query_time, tags=tags)
            infractions = list(infractions.value)
            total = sum(i[1] for i in infractions)
            content.append(u'\n**\u276F Infractions**')
            content.append('Total Infractions: {}'.format(total))
            content.append('Unique Servers: {}'.format(len(infractions)))

        if voice.value:
            statsd.timing('plugin.utilities.info.sql.voice', voice.value._query_time, tags=tags)
            voice = list(voice.value)
            content.append(u'\n**\u276F Voice**')
            content.append(u'Sessions: {}'.format(voice[0][1]))
            content.append(u'Time: {}'.format(humanize.naturaldelta(
                voice[0][2]
            )))

        embed = MessageEmbed()

        avatar = user.avatar
        if avatar:
            avatar = u'https://cdn.discordapp.com/avatars/{}/{}.{}'.format(
                user.id, avatar, u'gif' if avatar.startswith('a_') else u'png'
            )
        else:
            avatar = u'https://cdn.discordapp.com/embed/avatars/{}.png'.format(
                int(user.discriminator) % 5
            )

        embed.set_author(name=u'{}#{}'.format(
            user.username,
            user.discriminator,
        ), icon_url=avatar)

        embed.set_thumbnail(url=avatar)

        embed.description = '\n'.join(content)
        try:
            embed.color = get_dominant_colors_user(user, avatar)
        except:
            pass
        event.msg.reply('', embed=embed)

# --------------Coded by Xenthys#0001 for Rawgoat--------------

    def trigger_reminders(self):
        reminders = Reminder.with_message_join().where(
            (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))
        )

        waitables = []
        for reminder in reminders:
            waitables.append(self.spawn(self.trigger_reminder, reminder))

        for waitable in waitables:
            waitable.join()

        self.queue_reminders()

    def trigger_reminder(self, reminder):
        message = reminder.message_id
        channel = self.state.channels.get(message.channel_id)
        if not channel:
            self.log.warning('Not triggering reminder, channel %s was not found!',
                message.channel_id)
            reminder.delete_instance()
            return

        msg = channel.send_message(u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.format(
            message.author_id,
            reminder.created_at,
            humanize.naturaldelta(reminder.created_at - datetime.utcnow()),
            S(reminder.content)
        ))

        # Add the emoji options
        msg.add_reaction(SNOOZE_EMOJI)
        msg.add_reaction(GREEN_TICK_EMOJI)

        try:
            mra_event = self.wait_for_event(
                'MessageReactionAdd',
                message_id=msg.id,
                conditional=lambda e: (
                    (e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and
                    e.user_id == message.author_id
                )
            ).get(timeout=30)
        except gevent.Timeout:
            reminder.delete_instance()
            return
        finally:
            # Cleanup
            msg.delete_reaction(SNOOZE_EMOJI)
            msg.delete_reaction(GREEN_TICK_EMOJI)

        if mra_event.emoji.name == SNOOZE_EMOJI:
            reminder.remind_at = datetime.utcnow() + timedelta(minutes=20)
            reminder.save()
            msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.')
            return

        reminder.delete_instance()

    @Plugin.command('clear', group='r', global_=True)
    def cmd_remind_clear(self, event):
        count = Reminder.delete_for_user(event.author.id)
        return event.msg.reply(':ok_hand: I cleared {} reminders for you'.format(count))

    @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True)
    @Plugin.command('remind', '<duration:str> <content:str...>', global_=True)
    def cmd_remind(self, event, duration, content):
        if Reminder.count_for_user(event.author.id) > 30:
            return event.msg.reply(':warning: you an only have 15 reminders going at once!')

        remind_at = parse_duration(duration)
        if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)):
            return event.msg.reply(':warning: thats too far in the future, I\'ll forget!')

        r = Reminder.create(
            message_id=event.msg.id,
            remind_at=remind_at,
            content=content
        )
        self.reminder_task.set_next_schedule(r.remind_at)
        event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format(
            r.remind_at.isoformat(),
            humanize.naturaldelta(r.remind_at - datetime.utcnow()),
        ))

    @Plugin.command('messageinfo', '<mid:snowflake>', level=CommandLevels.MOD)
    def messageinfo(self, event, mid):
        try:
            msg = Message.select(Message).where(
                    (Message.id == mid)
                ).get()
        except Message.DoesNotExist:
            raise CommandFail('the id specified does not exist in our message database.')
        message_content = msg.content
        author_id = msg.author.id
        guild_id = msg.guild_id
        channel_id = msg.channel_id
        deleted = msg.deleted
        num_edits = msg.num_edits
        if num_edits > 0:
            num_edits_bool = True
        else:
            num_edits_bool = False
        discrim = str(msg.author.discriminator)
        # if len(str(cached_name[1])) != 4:
        #     while len(str(temp_str)) < 4:
        #         temp_str = '0' + str(temp_str)
        cached_name = str(msg.author.username) + '#' + str(discrim)
        avatar_name = msg.author.avatar 
        content = []
        embed = MessageEmbed()
        member = event.guild.get_member(author_id)
        
        if not avatar_name:
            if member:
                avatar = default_color(str(member.user.default_avatar))
            else:
                avatar = None   
        elif avatar_name.startswith('a_'):
            avatar = u'https://cdn.discordapp.com/avatars/{}/{}.gif'.format(author_id, avatar_name)
        else:
            avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(author_id, avatar_name)
        if member:
            embed.set_author(name='{} ({})'.format(member.user, member.id), icon_url=avatar)
            embed.set_thumbnail(url=avatar)
        else:
            if avatar:
                embed.set_author(name='{} ({})'.format(cached_name, author_id), icon_url=avatar)
                embed.set_thumbnail(url=avatar)
            else:
                embed.set_author(name='{} ({})'.format(cached_name, author_id))

        # embed.title = "Message Content:"
        content.append(u'**\u276F Message Information:**')
        content.append(u'In channel: <#{}>'.format(channel_id))
        content.append(u'Edited: **{}**'.format(num_edits_bool))
        if deleted:
            content.append(u'Deleted: **{}**'.format(deleted))
        content.append(u'Content: ```{}```'.format(message_content))
        if member:
            content.append(u'\n**\u276F Member Information**')

            if member.nick:
                content.append(u'Nickname: {}'.format(member.nick))

            content.append('Joined: {} ago ({})'.format(
                humanize.naturaldelta(datetime.utcnow() - member.joined_at),
                member.joined_at.isoformat(),
            ))

            if member.roles:
                roles = []
                for r in member.roles:
                    roles.append(member.guild.roles.get(r))
                roles = sorted(roles, key=lambda r: r.position, reverse=True)
                total = len(member.roles)
                roles = roles[:20]
                content.append(u'Roles ({}): {}{}'.format(
                    total, ' '.join(r.mention for r in roles),
                    ' (+{})'.format(total-20) if total > 20 else ''
                ))
                
        embed.description = '\n'.join(content)
        # embed.url = 'https://discordapp.com/channels/{}/{}/{}'.format(guild_id, channel_id, mid)
        embed.timestamp = datetime.utcnow().isoformat()
        if not event.author.avatar:
            auth_avatar = default_color(str(member.user.default_avatar))
        elif event.author.avatar.startswith('a_'):
            auth_avatar = u'https://cdn.discordapp.com/avatars/{}/{}.gif'.format(event.author.id, event.author.avatar)
        else:
            auth_avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(event.author.id, event.author.avatar)
        embed.set_footer(text='Requested by {}#{} ({})'.format(event.author.username, event.author.discriminator, event.author.id), icon_url=auth_avatar)
        try:
            embed.color = get_dominant_colors_user(member.user, avatar)
        except:
            embed.color = '00000000'
        event.msg.reply('', embed=embed)

    @Plugin.command('manager add', '<user:user> <item:str...>', level=-1, context={'mode': 'add'})
    @Plugin.command('manager remove', '<user:user> <item:str...>', level=-1, context={'mode': 'remove'})
    def manager_info(self, event, user, mode, item):
        user = self.state.users.get(user)
        if mode == 'add':
            special_list = rdb.hget('ServerManagers', '{}'.format(user.id))
            temp_list = []
            temp_list.append(item)
            final_list = str(temp_list).strip('[]')
            new = str('{}, {}'.format(special_list, final_list))
            rdb.hset('ServerManagers', '{}'.format(user.id), new)
            raise CommandSuccess('{} has been added to the list of server managers'.format(user))
        if mode == 'remove':
            special_list = rdb.hget('ServerManagers', '{}'.format(user.id))
            if special_list == None:
                raise CommandFail('User is not a manager on any Airplane protected servers.')
            temp_list = special_list.split(', ')
            found = False
            for x in temp_list:
                if x == item:
                    found = True
                    temp_list.remove(item)
            if found == False:
                raise CommandFail('something went wrong, please try again later')
            else:
                new = str(temp_list).strip('[]')
                rdb.hset('ServerManagers', '{}'.format(user.id), new)
                raise CommandSuccess('The server has been removed from the list of servers the user manages.')