Пример #1
0
    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.conf = Config('configs/nsfw.json')

        if 'update' not in self.conf:
            self.conf['update'] = {
                'safebooru': {
                    'url': 'https://safebooru.donmai.us'
                },
                'lolibooru': {
                    'url': 'https://lolibooru.moe'
                }
            }
            self.conf.save()
        pybooru.resources.SITE_LIST.update(self.conf['update'])

        if 'yandere-conf' not in self.conf:
            self.conf['yandere-conf'] = {}
        if 'danbooru-conf' not in self.conf:
            self.conf['danbooru-conf'] = {}
        if 'safebooru-conf' not in self.conf:
            self.conf['safebooru-conf'] = {}
        if 'lolibooru-conf' not in self.conf:
            self.conf['lolibooru-conf'] = {}

        self.yandere = pybooru.Moebooru('yandere', **self.conf['yandere-conf'])
        self.danbooru = pybooru.Danbooru('danbooru',
                                         **self.conf['danbooru-conf'])
        self.lolibooru = pybooru.Moebooru('lolibooru',
                                          **self.conf['lolibooru-conf'])
        self.safebooru = pybooru.Danbooru('safebooru',
                                          **self.conf['safebooru-conf'])
Пример #2
0
    def __init__(self, bot):
        self.bot = bot
        self.conf = Config('configs/heap.json')
        if 'heap' not in self.conf:
            self.conf['heap'] = heap.Heap()

        bot.loop.create_task(self.check_heap())
Пример #3
0
    def __init__(self, bot):
        self.bot = bot
        self.conf = Config('configs/server.json')
        self.cut = {}

        heap = self.bot.get_cog('HeapCog')
        for rem in self.conf.pop('end_role', []):
            self.bot.loop.run_until_complete(heap.push(rem))
Пример #4
0
  def __init__(self, bot):
    self.bot    = bot
    self.conf   = Config('configs/memes.json')
    doc  = self.meme.__dict__['help']
    doc += '\n  '
    doc += '\n  '.join(sorted(self.conf.get('memes', {}).keys()))

    self.meme.__dict__['help'] = doc
Пример #5
0
    def __init__(self, bot):
        self.conf = Config('configs/groupme.json')
        self.bot = bot
        self.loop = bot.loop
        self.l_bots = []
        self.g_bots = {}
        self.g_groups = {}
        self.d_chans = {}

        if 'g_old' not in self.conf:
            self.conf['g_old'] = {}
        if 'links' not in self.conf:
            self.conf['links'] = {}
        if 'key' not in self.conf or not self.conf['key']:
            self.conf['key'] = input(
                'Please enter your GroupMe api key: ').strip()

        if not self.conf['key']:
            raise RuntimeError('No groupme key provied')

        self.conf.save()

        groupy.config.API_KEY = self.conf['key']

        for discord_chan_id in self.conf['links']:
            for g_id in self.conf['links'][discord_chan_id]:
                group, g_bot = self.get_group_bot(g_id)

                #print('linked discord({}) to groupme({})'.format(discord_chan_id,g_id))

                if not group:
                    #print('could not find group')
                    continue

                channel = self.bot.get_channel(discord_chan_id)
                if not channel:
                    #print('error chan')
                    continue

                if g_id not in self.g_groups:
                    #print('new g_groups: {} -> {}'.format(g_id, str(group)))
                    self.l_bots.append(g_bot)
                    self.g_groups[g_id] = group

                if str(channel.id) in self.g_bots:
                    self.g_bots[str(channel.id)].append(g_bot)
                    #print('append g_bots: {}'.format(str(self.g_bots)))
                else:
                    self.g_bots[str(channel.id)] = [g_bot]
                    #print('new g_bots: {}'.format(str(self.g_bots)))

                if g_id in self.d_chans:
                    self.d_chans[g_id].append(channel)
                else:
                    self.d_chans[g_id] = [channel]

        self.loop.create_task(self.poll())
Пример #6
0
 def __init__(self, test=False):
     self.last = {}
     self.conf = Config('configs/az.json', save=(not test))
     if 'lenny' not in self.conf:
         self.conf['lenny'] = {}
     if 'img-reps' not in self.conf:
         self.conf['img-reps'] = {}
     if 'repeat_after' not in self.conf:
         self.conf['repeat_after'] = 3
     self.conf.save()
Пример #7
0
class TestConfig(unittest.TestCase):
    config_name = 'tests/test_config.json'
    config_name_alt = 'tests/test_save.json'

    def setUp(self):
        self.config = Config(TestConfig.config_name, save=False)

    def tearDown(self):
        Config.configs = {}
        if os.path.exists(TestConfig.config_name_alt):
            os.remove(TestConfig.config_name_alt)

    def test_empty(self):
        self.config = Config('does_not_exist', save=False)
        self.assertEqual(self.config, {}, "empty config not created")

    def test_keys(self):
        for key in "abcd":
            self.assertIn(key, self.config, "key missing in config")
        self.assertNotIn("e", self.config, "extra key present")

    def test_save(self):
        old_name = self.config.name
        self.config.name = TestConfig.config_name_alt
        self.config._save_force()
        self.config.name = old_name

        test_config = Config(TestConfig.config_name_alt, save=False)

        self.assertEqual(self.config, test_config, "saving does not work")

    def test_object_encode(self):  #TODO
        pass

    def test_object_decode(self):  #TODO
        pass

    def test_get(self):
        self.assertEqual(self.config.get("a"), 10, "get method failed")

    def test_brace_get(self):
        self.assertEqual(self.config["b"], [1, 2, 3],
                         "get using brackets failed")

    def test_delete(self):
        del self.config["a"]
        self.assertNotIn("a", self.config, "key not deleted")

    def test_set(self):
        self.config["new"] = "testing insert"
        self.assertEqual(self.config.get("new"), "testing insert",
                         "insert failed")
Пример #8
0
    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.conf = Config('configs/osu.json')

        if 'api-key' not in self.conf:
            self.conf['api-key'] = input('enter osu api key: ')
        if 'watched-users' not in self.conf:
            self.conf['watched-users'] = {}
        self.conf.save()

        self.api = osuapi.OsuApi(self.conf['api-key'],
                                 connector=osuapi.AHConnector())

        self.loop.create_task(self.check_scores())
Пример #9
0
class Games:
  def __init__(self, bot):
    self.bot  = bot
    self.conf = Config('configs/games.json')

  @perms.is_owner()
  @commands.command(aliases=['faa'])
  async def fake_artist_add(self, ctx, *, themes):
    self.conf['fake_artist']['themes'].extend(themes.strip().split('\n'))
    self.conf.save()
    await ctx.send(formatter.ok())

  @commands.command(aliases=['fa'])
  async def fake_artist(self, ctx, number : int):
    conf   = self.conf.get('fake_artist', {})
    themes = conf.get('themes', [])
    themes = random.sample(themes, len(themes)-len(themes)%number)
    output = [[] for i in range(number)]
    fakes  = list(range(number))*(len(themes)//number)
    random.shuffle(fakes)
    say = 'here are the links:'

    # generate
    for theme,fake in zip(themes, fakes):
      for i in range(len(output)):
        output[i].append(theme if i != fake else 'YOU ARE THE FAKE')

    # generate master file
    with open(os.path.join(conf.get('path',''), 'master.html'), 'w') as f:
      f.write(conf.get('rules'))
      for i,theme in enumerate(themes):
        f.write(f'''<li><input class="spoilerbutton" type="button"'''+ \
                f'''value="show" onclick="this.value=this.value=='show'''+ \
                f'''\'?'{html.escape(theme)}':'show';"></li>''')
      f.write(conf.get('out'))

    # generate player files
    for i in range(len(output)):
      filename = os.path.join(conf.get('path',''), f'{i+1}.html')
      with open(filename, 'w') as f:
        f.write(conf.get('rules'))
        for theme in output[i]:
          f.write(f'<li>{html.escape(theme)}</li>')
        f.write(conf.get('out'))
      say += f'\nhttps://andy29485.tk/files/{i+1}.html'

    await ctx.send(formatter.ok(say))
Пример #10
0
class MemeGenerator:
  pattern = re.compile(r'(\w+)\s+(.*)$')
  def __init__(self, bot):
    self.bot    = bot
    self.conf   = Config('configs/memes.json')
    doc  = self.meme.__dict__['help']
    doc += '\n  '
    doc += '\n  '.join(sorted(self.conf.get('memes', {}).keys()))

    self.meme.__dict__['help'] = doc

  @commands.command(aliases=['memes'])
  async def meme(self, ctx, *, text : str):
    """
    Adds text to images

    Valid names so far:
    """
    async with ctx.typing():
      match = MemeGenerator.pattern.match(text)
      name  = match.group(1).lower() if match else text
      text  = match.group(2) if match else ''
      text  = ' '.join(dh.remove_comments(text.split()))

      cfg = self.conf.get('memes', {}).get(name, None)

      if not cfg:
        await ctx.send(error('Could not find image'))
        return
      if not text:
        await ctx.send(error('Are you trying to get an empty image?'))
        return

      temp = tempfile.NamedTemporaryFile(suffix=".png")

      if 'font' not in cfg:
        cfg['font'] = self.conf.get('font', '')
      if 'path' not in cfg:
        cfg['path'] = self.conf.get('path', '')

      write_image(text, temp.name, **cfg)

      await ctx.send(file=discord.File(temp.name))

      temp.close()
Пример #11
0
async def on_message(message):
  logger.debug('global on message start')

  if message.author.bot:
    logger.debug('  ignoring message (reason: bot)')
    return

  # check if user is not the ignore list
  perms = Config('configs/perms.json')
  if str(message.author.id) in perms.get('ignore', []):
    logger.debug('  ignoring message (reason: user in ingore list)')
    return


  # check if command is a valid one
  if not re.search('^[\\.!\\?\\$]{2,}', message.content):
    await bot.process_commands(message)
  logger.debug('global on message end')
Пример #12
0
    def test_save(self):
        old_name = self.config.name
        self.config.name = TestConfig.config_name_alt
        self.config._save_force()
        self.config.name = old_name

        test_config = Config(TestConfig.config_name_alt, save=False)

        self.assertEqual(self.config, test_config, "saving does not work")
Пример #13
0
  def __init__(self, bot):
    self.bot  = bot
    self.info = {} # guild_id -> {queue, np}
    self.conn = emby_helper.conn
    self.conf = Config('configs/music.json')
    if 'volume' not in self.conf:
      self.conf['volume'] = {}

    self.bot.loop.create_task(self.update_db())
Пример #14
0
    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.stopwatches = {}
        self.conf = Config('configs/general.json')

        heap = self.bot.get_cog('HeapCog')

        if 'responses' not in self.conf:
            self.conf['responses'] = {}
        if 'todo' not in self.conf:
            self.conf['todo'] = {}
        if 'situations' not in self.conf:
            self.conf['situations'] = []
        if '8-ball' not in self.conf:
            self.conf['8-ball'] = []
        for rem in self.conf.pop('reminders', []):
            self.loop.run_until_complete(heap.push(rem, None))
        self.conf.save()
Пример #15
0
class HeapCog:
    def __init__(self, bot):
        self.bot = bot
        self.conf = Config('configs/heap.json')
        if 'heap' not in self.conf:
            self.conf['heap'] = heap.Heap()

        bot.loop.create_task(self.check_heap())

    async def check_heap(self):
        while self == self.bot.get_cog('HeapCog'):
            heap_popped = False
            # if there are valid items that expired/expire soon, process them
            while self.conf['heap'].time_left < 2:
                # remove item from heap
                item = self.conf['heap'].pop()
                logger.debug('popping item from heap (%s)', item.to_dict())

                await item.end(self.bot)  # perform its task
                heap_popped = True  # signify that a save is needed

            # only save heap to disk if an item was pop
            if heap_popped:
                self.conf.save()

            # wait a bit and check again
            await asyncio.sleep(min(self.conf['heap'].time_left, 30) + 0.5)

    def index(self, item):
        return self.conf['heap'].index(item)

    async def push(self, item, *args, **kwargs):
        self.conf['heap'].push(item)
        await item.begin(*args, **kwargs)
        self.conf.save()

    def pop(self, item):
        self.conf['heap'].pop(item)
        self.conf.save()

    def __iter__(self):
        return self.conf['heap'].__iter__()
Пример #16
0
 def __init__(self, bot):
     self.bot = bot
     self.conf = Config('configs/dnd.json')
Пример #17
0
 def __init__(self, test=False):
     self.last = {}
     self.conf = Config('configs/math.json', save=(not test))
Пример #18
0
 def test_empty(self):
     self.config = Config('does_not_exist', save=False)
     self.assertEqual(self.config, {}, "empty config not created")
Пример #19
0
 def setUp(self):
     self.config = Config(TestConfig.config_name, save=False)
Пример #20
0
    0x3498db,
    0x206694,
    0x9b59b6,
    0x71368a,
    0xe91e63,
    0xe67e22,
    0xf1c40f,
    0x1abc9c,
    0x2ecc71,
    0xa84300,
    0xe74c3c,
    0xad1457,
    0x11806a,
]

conf = Config('configs/emby.json')

if 'address' not in conf or not conf['address']:
    conf['address'] = input('Enter emby url: ')
    conf.save()
if 'watching' not in conf or 'last' not in conf['watching']:
    conf['watching'] = {'last': None}
    conf.save()
if 'auth' not in conf or not conf['auth']:
    conf['auth'] = {}
    conf['auth']['api_key'] = input('Enter emby api key: ')
    conf['auth']['userid'] = input('Enter emby user id: ')
    conf['auth']['device_id'] = input('Enter emby device id: ')
    conf.save()

conn = EmbyPy(conf['address'], **conf['auth'], ws=False)
Пример #21
0
class Server:
    def __init__(self, bot):
        self.bot = bot
        self.conf = Config('configs/server.json')
        self.cut = {}

        heap = self.bot.get_cog('HeapCog')
        for rem in self.conf.pop('end_role', []):
            self.bot.loop.run_until_complete(heap.push(rem))

    @perms.pm_or_perms(manage_messages=True)
    @commands.command(name='prune')
    async def _prune(self, ctx, num_to_delete: int, *message):
        """
    deletes specified number of messages from channel
    if message is specified, message will be echoed by bot after prune

    USAGE: .prune <num> [user] [message...]

    NOTE: if first word after number is a user,
          only user's messages will be pruned
    """
        # tmp channel/server pointer
        chan = ctx.message.channel
        serv = ctx.message.guild

        #if num_to_delete > 100:                       # api only allows up to 100
        #  await ctx.send('Sorry, only up to 100') # TODO - copy thing done in
        #  return                                      #        self._paste
        if num_to_delete < 1:  # delete nothing?
            await ctx.send('umm... no')  #  answer: no
            return

        # if the first word in the message matches a user,
        #   remove that word from the message, store the user
        try:
            user = dh.get_user(serv or self.bot, message[0])
            if user:
                message = message[1:]
        except:
            logger.debug('did not match a user')
            user = None

        check = lambda m: True
        if user:  # if a user was matched, delete messages for that user only
            logger.debug(f'pruning for user {user.name}')
            check = lambda m: str(m.author.id) == str(user.id)

        message = ' '.join(message)  #make the message a string

        logs = []
        async for m in chan.history(limit=num_to_delete, reverse=True):
            if check(m):
                logs.append(m)

        deleted = len(logs)
        old = False
        while len(logs) > 0:  # while there are messages to delete
            if len(logs) > 1:  #   if more than one left to delete and not old,
                if not old:  #     attempt batch delete [2-100] messages
                    try:
                        await chan.delete_messages(logs[:100])
                    except:  #   if problem when batch deleting
                        old = True  #     then the messages must be old
                if old:  # if old, traverse and delete individually
                    for entry in logs[:100]:
                        try:
                            await entry.delete()
                        except:
                            logger.exception(
                                '<{0.author.name}> {0.content}'.format(entry))
                logs = logs[100:]
            else:  # if only one message, delete individually
                await logs[0].delete()
                logs.remove(logs[0])

        #report that prume was complete, how many were prunned, and the message
        await ctx.send(
            ok('Deleted {} message{} {}'.format(
                deleted, '' if deleted == 1 else 's',
                f'({message})' if message else '')))

    @commands.group(name='role', aliases=['give', 'giveme', 'gimme'])
    async def _role(self, ctx):
        """
    Manage publicly available roles
    """
        # if no sub commands were called, guess at what the user wanted to do
        if ctx.invoked_subcommand is None:
            msg = ctx.message.content.split()  # attempt to parse args
            if len(msg) < 2:
                await ctx.send('see help (`.help role`)')
                return
            role = msg[1]
            date = ' '.join(msg[2:])

            # if the user cannot manage roles, then they must be requesting a role
            #   or they are trying to do something that they are not allowed to
            if not perms.check_permissions(ctx.message, manage_roles=True):
                await self._request_wrap(ctx, role,
                                         date)  # attempt to request role
                return

            #if the user does have permission to manage, they must be an admin/mod
            #  ask them what they want to do - since they clearly did not know what
            #  they were trying to do
            await ctx.send('Are you trying to [a]dd a new role ' + \
                               'or are you [r]equesting this role for yourself?'
            )
            try:  # wait for them to reply

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

                msg = await self.bot.wait_for('message',
                                              timeout=30,
                                              check=check)
            except:  # if they do not reply, give them a helpful reply
                #   without commenting on their IQ
                await ctx.send(
                    error('Response timeout, maybe look at the help?'))
                return
            # if a reply was recived, check what they wanted to do and pass along
            msg = msg.content.lower()
            if msg.startswith('a') or 'add' in msg:  # adding new role to list
                await self._add_wrap(ctx, role)
                reply = f"Please run `.role request {role}` to get the \"{role}\" role"
                await ctx.send(reply)
            elif msg.startswith(
                    'r') or 'request' in msg:  # requesting existing role
                await self._request_wrap(ctx, role, date)
            else:  # they can't read
                await ctx.send(error('I have no idea what you are attempting' + \
                                         ' to do, maybe look at the help?')
                )

    @_role.command(name='add', aliases=['create', 'a'])
    @perms.has_perms(manage_roles=True)
    async def _add(self, ctx, role: str):
        """
    adds role to list of public roles
    """
        await self._add_wrap(ctx, role)

    @_role.command(name='list', aliases=['ls', 'l'])
    async def _list(self, ctx):
        """
    lists public roles avalible in the server
    """

        # pull roles out of the config file
        serv = ctx.message.guild
        names = []
        m_len = 0
        available_roles = self.conf.get(str(serv.id), {}).get('pub_roles', [])

        # if no roles, say so
        if not available_roles:
            await ctx.send('no public roles in this server\n' + \
                               ' see `.help role create` and `.help role add`'
            )
            return

        # For each id in list
        #   find matching role in server
        #   if role exists, add it to the role list
        # Note: this block also finds the strlen of the longest role name,
        #       this will be used later for formatting
        for role_id in available_roles:
            role = discord.utils.find(lambda r: str(r.id) == role_id,
                                      serv.roles)
            if role:
                names.append(role.name)
                m_len = max(m_len, len(role.name))

        # create a message with each role name and id on a seperate line
        # seperators(role - id) should align due to spacing - this is what the
        #   lenght of the longest role name is used for
        msg = 'Roles:\n```'
        line = '{{:{}}} - {{}}\n'.format(m_len)
        for name, rid in zip(names, available_roles):
            msg += line.format(name, rid)

        # send message with role list
        await ctx.send(msg + '```')

    @_role.command(name='remove', aliases=['rm'])
    @perms.has_perms(manage_roles=True)
    async def _delete(self, ctx, role: str):
        """
    removes role from list of public roles
    """

        # attempt to find specified role and get list of roles in server
        serv = ctx.message.guild
        role = dh.get_role(serv, role)
        guild_id = str(serv.id)
        role_id = str(role.id)
        available_roles = self.conf.get(guild_id, {}).get('pub_roles', [])

        # if user failed to specify role, complain
        if not role:
            await ctx.send('Please specify a valid role')
            return

        if guild_id not in self.conf:
            self.conf[guild_id] = {'pub_roles': []}
            self.conf.save()
        elif 'pub_roles' not in self.conf[guild_id]:
            self.conf[guild_id]['pub_roles'] = []
            self.conf.save()

        if role_id in available_roles:  # if role is found, remove and report
            self.conf[guild_id]['pub_roles'].remove(guild_id)
            self.conf.save()
            await ctx.send(ok('role removed from public list'))
        else:  # if role is not in server, just report
            await ctx.send(error('role is not in the list'))

    @_role.command(name='request', aliases=['r'])
    async def _request(self, ctx, role: str, date: str = ''):
        """
    adds role to requester(if in list)
    """
        await self._request_wrap(ctx, role, date)

    @_role.command(name='unrequest', aliases=['rmr', 'u'])
    async def _unrequest(self, ctx, role: str):
        """removes role from requester(if in list)"""

        # attempt to find role that user specied for removal
        auth = ctx.message.author
        serv = ctx.message.guild
        role = dh.get_role(serv, role)
        guild_id = str(serv.id)
        role_id = str(role.id)

        # if user failed to find specify role, complain
        if not role:
            await ctx.send('Please specify a valid role')
            return

        # get a list of roles that are listed as public and the user roles
        available_roles = self.conf.get(guild_id, {}).get('pub_roles', [])
        user_roles = discord.utils.find(lambda r: str(r.id) == role_id,
                                        auth.roles)

        # ONLY remove roles if they are in the public roles list
        # Unless there is no list,
        #   in which case any of the user's roles can be removed
        if role_id in available_roles or user_roles:
            await auth.remove_roles(role)
            await ctx.send(ok('you no longer have that role'))
        else:
            await ctx.send(error('I\'m afraid that I can\'t remove that role'))

    # wrapper function for adding roles to public list
    async def _add_wrap(self, ctx, role):
        serv = ctx.message.guild

        # find the role,
        # if it is not found, create a new role
        role_str = role
        if type(role) != discord.Role:
            role = dh.get_role(serv, role_str)
        if not role:
            role = await serv.create_role(name=role_str, mentionable=True)
            await ctx.send(ok(f'New role created: {role_str}'))

        # if still no role, report and stop
        if not role:
            await ctx.send(error("could not find or create role role"))
            return

        guild_id = str(serv.id)
        role_id = str(role.id)

        # The @everyone role (also @here iiuc) cannot be given/taken
        if role.is_everyone:
            await ctx.send(error('umm... no'))
            return

        if guild_id not in self.conf:  # if server does not have a list yet create it
            self.conf[guild_id] = {'pub_roles': [role_id]}
        elif 'pub_roles' not in self.conf[guild_id]:  # if list is corruptted
            self.conf[guild_id]['pub_roles'] = [role_id]  # fix it
        elif role_id in self.conf[guild_id][
                'pub_roles']:  # if role is already there
            await ctx.send('role already in list')  #   report and stop
            return
        else:  # otherwise add it to the list and end
            self.conf[guild_id]['pub_roles'].append(role_id)

        # save any changes to config file, and report success
        self.conf.save()
        await ctx.send(ok('role added to public role list'))

    # wrapper function for getting roles that are on the list
    async def _request_wrap(self, ctx, role, date=''):
        auth = ctx.message.author
        chan = ctx.message.channel
        serv = ctx.message.guild

        # attempt to find the role if a string was given,
        #   if not found, stop
        if type(role) != discord.Role:
            role = dh.get_role(serv, role)
        if not role:
            await ctx.send(error("could not find role, ask a mod to create it")
                           )
            return

        # get list of public roles
        available_roles = self.conf.get(guild_id, {}).get('pub_roles', [])
        role_id = str(role.id)
        guild_id = str(serv.id)

        if role_id in available_roles:  # if role is a public role,
            await auth.add_roles(role)  #   give it
            await ctx.send(ok('you now have that role'))
        else:  # otherwise don't
            await ctx.send(
                error('I\'m afraid that I can\'t give you that role'))
            return

        if date:  # if a timeout was specified
            end_time = dh.get_end_time(date)[0]
            role_end = RoleRemove(end_time, role_id, str(auth.id),
                                  str(chan.id), guild_id)

            await self.bot.get_cog('HeapCog').push(role_end, self.bot)

    @perms.pm_or_perms(manage_messages=True)
    @commands.command(name='cut')
    async def _cut(self, ctx, cut: str, skip: str = ''):
        '''
    cuts num_to_cut messages from the current channel
    skips over num_to_skip messages (skips none if not specified)

    example:
    User1: first message
    User2: other message
    User3: final message
    Using ".cut 1"             will cut User3's message
    Using ".cut 1 1"           will cut User2's message
    Using ".cut 3"             will cut all messages
    Using ".cut 3:other"       will cut User2 and 3's messages
    Using ".cut id:XXX"        will cut id XXX
    Using ".cut id:XXX id:YYY" will cut messages in range (id XXX, id YYY]

    messages will not be deleted until paste
    needs manage_messages perm in the current channel to use
    see .paste
    '''
        #if num_to_cut > 100:
        #  await ctx.send('Sorry, only up to 100')
        #  return
        in_id = re.search('^id:(\\d+)$', cut)
        in_re = re.search('^(\d+):(.+)$', cut)
        if in_id:
            cut = await ctx.message.channel.get_message(int(in_id.group(1)))
        elif in_re:
            cut = int(in_re.group(1))
            in_re = re.compile(in_re.group(2))
        elif re.search('^\d+$', cut):
            cut = int(cut)
        else:
            await ctx.send(error('bad cut parameter'))
            return

        skip_id = re.search('^id:(\\d+)$', skip)
        skip_re = re.search('^(\d+):(.+)$', skip)
        if skip_id:
            skip = await ctx.message.channel.get_message(int(skip_id.group(1)))
        elif skip_re:
            skip = int(skip_re.group(1))
            skip_re = re.compile(skip_re.group(2))
        elif not skip or re.search('^\d+$', skip):
            skip = int(skip or '0')
        else:
            await ctx.send(error('bad skip parameter'))
            return

        if not cut or (type(cut) == int and cut < 1):  # can't cut no messages
            await ctx.send('umm... no')
            return

        # store info in easier to access variables
        chan = ctx.message.channel
        bef = ctx.message.created_at
        aid = str(ctx.message.author.id)
        cid = str(chan.id)

        # delete the original `.cut` message(not part of cutting)
        # also sorta serves as confirmation that messages have been cut
        await ctx.message.delete()

        # if messages should be skipped when cutting
        # save the timestamp of the oldest message
        if skip:
            if type(skip) == int:
                run = lambda: chan.history(limit=skip, reverse=True)
            else:
                run = lambda: chan.history(after=skip, reverse=True)

            async for m in run():
                if skip_re and not skip_re.search(m.content):
                    continue
                bef = m.created_at
                break

        # save the logs to a list
        #store true in position 0 of list if channel is a nsfw channel
        logs = ['nsfw' in chan.name.lower()]
        if type(cut) == int:
            run = lambda: chan.history(limit=cut, before=bef, reverse=True)
        else:
            run = lambda: chan.history(after=cut, before=bef, reverse=True)

        async for m in run():
            if in_re and in_re.search(m.content):
                in_re = False
            elif in_re:
                continue
            logs.append(m)

        # save logs to dict (associate with user)
        self.cut[aid] = logs

    @perms.has_perms(manage_messages=True)
    @commands.command(name='paste')
    async def _paste(self, ctx):
        '''
    paste cutted messages to current channel

    needs manage_messages perm in the current channel to use
    deletes original messages
    see .cut
    '''
        # get messages that were cut and other variables
        aid = str(ctx.message.author.id)
        chan = ctx.message.channel
        logs = self.cut.pop(aid, [])

        # if nothing was cut, stop
        if not logs:
            await ctx.send('You have not cut anything')
            return

        # it messages were cut in a nsfw channel,
        #   do not paste unless this is a nsfw channel
        # NOTE: cutting/pasting to/from PMs is not possible(for now)
        if logs[0] and 'nsfw' not in chan.name.lower():
            await ctx.send('That which hath been cut in nsfw, ' + \
                               'mustn\'t be pasted in such a place'
            )
            return

        # remove the nsfw indicator(since it's not really part of the logs)
        logs = logs[1:]

        # delete the `.paste` message
        await ctx.message.delete()

        # compress the messages - many messages can be squished into 1 big message
        # but ensure that output messages do not exceede the discord message limit
        buf = ''  # current out message that is being compressed to
        out = []  # output messages that have been compressed
        for message in logs:
            # save messages as:
            #   <nick> message
            # and attachments as(after the message):
            #   filename: url_to_attachment
            if message.content or message.attachments:
                tmp = '<{0.author.name}> {0.content}\n'.format(message)
                for a in message.attachments:
                    tmp += '{0.filename}: {0.url}\n'.format(a)
            else:
                tmp = ''
            # if this message would make the current output buffer too long,
            #   append it to the output message list and reset the buffer
            # or just append to the buffer if it's not going to be too long
            if len(buf) + len(tmp) > 1900:
                out.append(buf)
                buf = tmp
            else:
                buf += tmp

            # if the message is composed of *only* embeds,
            #   flush buffer,
            #   and append embed to output list
            if message.embeds and not message.content:
                if buf:
                    out.append(buf)
                    buf = ''
                for embed in message.embeds:
                    out.append(embed)

        # if there is still content in the buffer after messages have been traversed
        #   treat buffer as complete message
        if buf:
            out.append(buf)

        # send each message in output list
        for mes in out:
            if type(mes) == str:
                if mes:
                    await ctx.send(mes)
            else:  # if it's an embed, n
                await ctx.send(embed=EmWrap(mes))  #   it needs to be wrapped

        # once all messages have been pasted, delete(since cut) the old ones

        old = False  # messages older than 2 weeks cannot be batch deleted

        while len(logs) > 0:  # while there are messages to delete
            if len(logs) > 1:  #   if more than one left to delete and not old,
                if not old:  #     attempt batch delete [2-100] messages
                    try:
                        await chan.delete_messages(logs[:100])
                    except:  #   if problem when batch deleting
                        old = True  #     then the messages must be old
                if old:  # if old, traverse and delete individually
                    for entry in logs[:100]:
                        await entry.delete()
                logs = logs[100:]
            else:  # if only one message, delete individually
                await logs[0].delete()
                logs.remove(logs[0])

        # remove cut entry from dict of cuts
        if aid in self.cut:
            del self.cut[aid]

    @commands.command(name='topic')
    async def _topic(self, ctx, *, new_topic=''):
        """manage topic

    if a new_topic is specified, changes the topic
    otherwise, displays the current topic
    """
        # store channel in tmp pointer
        c = ctx.message.channel

        if new_topic:
            # if a topic was passed,
            #   change it if user has the permisssions to do so
            #   or tell user that they can't do that
            if perms.check_permissions(ctx.message, manage_channels=True):
                await c.edit(topic=new_topic)
                await ctx.send(
                    ok('Topic for #{} has been changed'.format(c.name)))
            else:
                await ctx.send(
                    error('You cannot change the topic for #{}'.format(c.name))
                )
        elif c.topic:
            # if no topic has been passed,
            #   say the topic
            await ctx.send('Topic for #{}: `{}`'.format(c.name, c.topic))
        else:
            # if not topic in channel,
            #   say so
            await ctx.send('#{} has no topic'.format(c.name))

    @perms.has_perms(manage_roles=True)
    @commands.command(name='timeout_send', aliases=['ts'])
    async def _timeout_send(self,
                            ctx,
                            member: discord.Member,
                            time: float = 300):
        """puts a member in timeout for a duration(default 5 min)

    usage `.timeout [add] @member [time in seconds]`
    """
        heap = self.bot.get_cog('HeapCog')
        if not perms.is_owner() and \
          ctx.message.author.server_permissions < member.server_permissions:
            await ctx.send('Can\'t send higher ranking members to timeout')
            return

        server = ctx.message.guild
        channel = ctx.message.channel

        if perms.in_group('timeout') and not perms.is_owner():
            await ctx.send('You\'re in timeout... No.')
            return

        if not ctx.message.guild:
            await ctx.send('not in a server at the moment')
            return

        if time < 10:
            await ctx.send('And what would the point of that be?')
            return

        if time > 10000:
            await ctx.send('Too long, at this point consider banning them')
            return

        criteria = lambda m: re.search('(?i)^time?[ _-]?out.*', m.name)

        to_role = discord.utils.find(criteria, server.roles)
        to_chan = discord.utils.find(criteria, server.channels)

        try:
            timeout_obj = Timeout(channel, server, member, time)
            await heap.push(timeout_obj, self.bot, to_role, to_chan)
        except:
            for index, obj in enumerate(heap):
                if obj == timeout_obj:
                    heap.pop(index)
                    break
            await ctx.send(
                'There was an error sending {}\'s to timeout \n({}{}\n)'.
                format(
                    member.name,
                    '\n  - do I have permission to manage roles(and possibly channels)?',
                    '\n  - is my highest role above {}\'s highest role?'.
                    format(member.name)))
            #raise

    @perms.has_perms(manage_roles=True)
    @commands.command(name='timeout_end', aliases=['te'])
    async def _timeout_end(self, ctx, member: discord.Member):
        """removes a member from timeout

    usage `.timeout end @member`
    """
        server = ctx.message.guild
        channel = ctx.message.channel

        if perms.in_group('timeout') and not perms.is_owner():
            await ctx.send('You\'re in timeout... No.')
            return

        if not ctx.message.guild:
            await ctx.send('not in a server at the moment')
            return

        # test timeout object for comparison
        test = namedtuple({
            'server_id': int(server.id),
            'user_id': int(member.id)
        })
        index = 0  # inext is used to more efficently pop from heap

        # error message in case ending timeout fails
        error_msg = 'There was an error ending {}\'s timeout \n({}{}\n)'.format(
            member.name,
            '\n  - do I have permission to manage roles(and possibly channels)?',
            '\n  - is my highest role above {}\'s highest role?'.format(
                member.name))

        for timeout in Timeout.conf['timeouts']:  # look trhough all timouts
            if timeout == test:  #   if found
                try:
                    await timeout.end(self.bot, index)  #     attempt to end
                except:
                    await ctx.send(error_msg
                                   )  #     if error when ending, report
                return
            index += 1  #   not found increment index
        else:  # not found at all, report
            await ctx.send('{} is not in timeout...'.format(member.name))
            return

    # checks timeouts and restores perms when timout expires
    async def check_timeouts(self):
        if 'timeouts' not in Timeout.conf:  #create timeouts list if needed
            Timeout.conf['timeouts'] = []

        while self == self.bot.get_cog('Server'):  # in case of cog reload
            # while timeouts exist, and the next one's time has come,
            #   end it
            while Timeout.conf['timeouts'] and \
                  Timeout.conf['timeouts'][0].time_left < 1:
                await Timeout.conf['timeouts'][0].end(self.bot, 0)

            # wait a bit and check again
            #   if the next one ends in < 15s, wait that much instead of 15s
            if Timeout.conf['timeouts']:
                delay = min(Timeout.conf['timeouts'].time_left, 15)
            else:
                delay = 15
            await asyncio.sleep(delay + 0.5)
Пример #22
0
#!/usr/bin/env python3

import zipfile
import tempfile
import logging
import random
import os
import re
from includes.utils.config import Config

logger = logging.getLogger('navi.find')

# load config, for search term replacements
conf = Config('configs/az.json')

def search(directory, patterns, single=True):
  '''
  searches all file in a directory for a set of patterns

  if a pattern starts with a hyphen("-") it will be negatively matched

  if "single" is false, all matched are returned
  otherwise only one will be returned at random(default behaviour)

  Note: if a ".git" dir is present, it will not be searched
  '''
  # remove duplicates from pattern,
  # convert all strings to lowercase,
  # and remove empty strings
  patterns = set(x for x in patterns if x)
  debugPts = set()
Пример #23
0
class NSFW:
    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.conf = Config('configs/nsfw.json')

        if 'update' not in self.conf:
            self.conf['update'] = {
                'safebooru': {
                    'url': 'https://safebooru.donmai.us'
                },
                'lolibooru': {
                    'url': 'https://lolibooru.moe'
                }
            }
            self.conf.save()
        pybooru.resources.SITE_LIST.update(self.conf['update'])

        if 'yandere-conf' not in self.conf:
            self.conf['yandere-conf'] = {}
        if 'danbooru-conf' not in self.conf:
            self.conf['danbooru-conf'] = {}
        if 'safebooru-conf' not in self.conf:
            self.conf['safebooru-conf'] = {}
        if 'lolibooru-conf' not in self.conf:
            self.conf['lolibooru-conf'] = {}

        self.yandere = pybooru.Moebooru('yandere', **self.conf['yandere-conf'])
        self.danbooru = pybooru.Danbooru('danbooru',
                                         **self.conf['danbooru-conf'])
        self.lolibooru = pybooru.Moebooru('lolibooru',
                                          **self.conf['lolibooru-conf'])
        self.safebooru = pybooru.Danbooru('safebooru',
                                          **self.conf['safebooru-conf'])

    @commands.group()
    @commands.is_nsfw()
    async def nsfw(self, ctx):
        """NSFW stuff"""
        channel = ctx.message.channel
        # # ensure that the current channel is marked as nsfw
        # if not channel.is_private and 'nsfw' not in channel.name.lower():
        #   await ctx.send(formatter.error('not in nsfw channel'))
        #   ctx.invoked_subcommand = None
        #   return

        # if user misstyped or does not know what they are doing, complain
        if ctx.invoked_subcommand is None:
            await ctx.send(formatter.error("Please specify valid subcommand"))
            return

    @nsfw.command(name='danbooru', aliases=['d'])
    async def _danbooru(self, ctx, *, search_tags: str = ''):
        """
      searches danbooru for an image

      usage: .nsfw danbooru [num] tags1 tag2, tag_3, etc...
      (optional) num: number of posts to show [1,5]
      if no tags are given, rating:e is assumed
      will potentially return nsfw images
    """
        tags = re.split(',?\\s+', search_tags)
        tags = dh.remove_comments(tags)

        if len(tags) > 1 and re.match('\\d+$', tags[0]):
            num = min(5, max(1, int(tags[0])))
            tags = tags[1:]
        else:
            num = 1

        if not tags:
            tags = ['rating:e']

        tags = ' '.join(tags)
        get = lambda: self.danbooru.post_list(
            limit=num, tags=tags, random=True)
        posts = await self.loop.run_in_executor(None, get)

        if not posts:
            await ctx.send('could not find anything')
            return

        for post in posts:
            em = Embed()
            em.title = search_tags or 'rating:e'
            em.url = 'https://danbooru.donmai.us/posts/{}'.format(post['id'])

            if 'large_file_url' in post:
                u = post['large_file_url']
            elif 'file_url' in post:
                u = post['file_url']
            else:
                await ctx.send('''
                Sorry, there seems to be a premium tag in the image,
                send me $20 if you you want to search it.
        ''')
            if 'http' not in u:
                u = f'https://danbooru.donmai.us/{u}'
            em.set_image(url=u)

            tags = post['tag_string']
            if len(tags) > 700:
                tags = tags[:696] + ' ...'
            if tags:
                em.set_footer(text=tags)

            await ctx.send(embed=em)

    @nsfw.command(name='lolibooru', aliases=['l'])
    async def _lolibooru(self, ctx, *, search_tags: str = ''):
        """
      searches lolibooru for an image

      usage: .nsfw lolibooru [num] tags1 tag2, tag_3, etc...
      (optional) num: number of posts to show [1,5]
      if no tags are given, rating:e is assumed
      will potentially return nsfw images
    """
        tags = re.split(',?\\s+', search_tags)
        tags = dh.remove_comments(tags)

        if len(tags) > 1 and re.match('\\d+$', tags[0]):
            num = min(5, max(1, int(tags[0])))
            tags = tags[1:]
        else:
            num = 1

        if not tags:
            tags = ['rating:e']

        tags = ' '.join(tags)
        get = lambda: self.lolibooru.post_list(limit=100, tags=tags)
        posts = await self.loop.run_in_executor(None, get)

        if not posts:
            await ctx.send('could not find anything')
            return

        for i in range(num):
            if not posts:
                break
            post = random.choice(posts)
            posts.remove(post)
            em = Embed()
            em.title = search_tags or 'rating:e'
            em.url = 'https://lolibooru.moe/post/show/{}'.format(post['id'])
            u = post['file_url'].replace(' ', '%20')
            em.set_image(url=u)

            tags = post['tags']
            if len(tags) > 700:
                tags = tags[:696] + ' ...'
            if tags:
                em.set_footer(text=tags)

            await ctx.send(embed=em)

    @nsfw.command(name='yandere', aliases=['y'])
    async def _yandre(self, ctx, *, search_tags: str = '' or 'rating:e'):
        """
      searches yande.re for an image

      usage: .nsfw yandere [num] tags1 tag2, tag_3, etc...
      (optional) num: number of posts to show [1,5]
      if no tags are given, rating:e is assumed
      will potentially return nsfw images
    """
        tags = re.split(',?\\s+', search_tags)
        tags = dh.remove_comments(tags)

        if len(tags) > 1 and re.match('\\d+$', tags[0]):
            num = min(5, max(1, int(tags[0])))
            tags = tags[1:]
        else:
            num = 1

        if not tags:
            tags = ['rating:e']

        tags = ' '.join(tags)
        get = lambda: self.yandere.post_list(limit=100, tags=tags)
        posts = await self.loop.run_in_executor(None, get)

        if not posts:
            await ctx.send('could not find anything')
            return

        for i in range(num):
            if not posts:
                break
            post = random.choice(posts)
            posts.remove(post)
            em = Embed()
            em.title = search_tags or 'rating:e'
            em.url = 'https://yande.re/post/show/{}'.format(post['id'])
            u = post['file_url']
            em.set_image(url=u)

            tags = post['tags']
            if len(tags) > 700:
                tags = tags[:696] + ' ...'
            if tags:
                em.set_footer(text=tags)

            await ctx.send(embed=em)

    @commands.command(name='safebooru', aliases=['s', 'safe'])
    async def _safebooru(self, ctx, *, search_tags: str):
        """
      searches safebooru for an image

      usage: .safebooru [num] tags1 tag2, tag_3, etc...
      (optional) num: number of posts to show [1,5]
      at least 1 tag must be specified
      will potentially return nsfw images
    """
        tags = re.split(',?\\s+', search_tags)
        tags = dh.remove_comments(tags)

        if len(tags) > 1 and re.match('\\d+$', tags[0]):
            num = min(5, max(1, int(tags[0])))
            tags = tags[1:]
        else:
            num = 1

        tags = ' '.join(tags)
        get = lambda: self.safebooru.post_list(
            limit=num, tags=tags, random=True)
        posts = await self.loop.run_in_executor(None, get)

        if not posts:
            await ctx.send('could not find anything')
            return

        for post in posts:
            em = Embed()
            em.title = search_tags
            em.url = 'https://safebooru.donmai.us/posts/{}'.format(post['id'])
            if 'large_file_url' in post:
                u = post['large_file_url']
            elif 'file_url' in post:
                u = post['file_url']
            else:
                await ctx.send('''
                Sorry, there seems to be a premium tag in the image,
                send me $20 if you you want to search it.
        ''')
                return
            if 'http' not in u:
                u = 'https://safebooru.donmai.us' + u
            em.set_image(url=u)

            tags = post['tag_string']
            if len(tags) > 700:
                tags = tags[:696] + ' ...'
            if tags:
                em.set_footer(text=tags)

            await ctx.send(embed=em)
Пример #24
0
class Regex:
    def __init__(self, test=False):
        self.replacements = Config('configs/replace.json', save=(not test))
        self.permissions = Config('configs/perms.json', save=(not test))
        if 'rep-blacklist' not in self.permissions:
            self.permissions['rep-blacklist'] = []

    def add(self, regex, author_id=''):
        #Find requested replacement
        rep = get_match(regex)

        #ensure that replace was found before proceeding
        if not rep:
            return formatter.error('Could not find valid regex')

        p1 = formatter.escape_mentions(rep.group(2))
        p2 = formatter.escape_mentions(rep.group(4))

        #check regex for validity
        if not comp(p1, p2):
            return formatter.error('regex is invalid')

        #make sure that there are no similar regexes in db
        for i in self.replacements:
            if similar(p1, i):
                r = '\"{}\" -> \"{}\"'.format(i, self.replacements[i][0])
                message = 'Similar regex already exists, delete or edit it\n{}'.format(
                    formatter.inline(r))
                return formatter.error(message)

        #make sure regex is not too broad
        if bad_re(p1):
            return formatter.error('regex is too broad')

        #check that regex does not already exist
        if p1 in self.replacements:
            return formatter.error('regex already exists')

        self.replacements[p1] = [p2, author_id]
        return formatter.ok()

    def edit(self, regex, author_id=''):
        #Find requested replacement
        rep = get_match(regex)

        #ensure that replace was found before proceeding
        if not rep:
            return formatter.error('Could not find valid regex')

        p1 = formatter.escape_mentions(rep.group(2))
        p2 = formatter.escape_mentions(rep.group(4))

        #check regex for validity
        if not comp(p1, p2):
            return formatter.error('regex is invalid')

        #make sure regex is not too broad
        if bad_re(p1):
            return formatter.error('regex is too broad')

        #ensure that replace was found before proceeding
        if p1 not in self.replacements:
            return formatter.error('Regex not in replacements.')

        #check if they have correct permissions
        if author_id != self.replacements[p1][1] \
           and not perms.is_owner_check(author_id):
            #will uncomment next line when reps are a per server thing
            #and not perms.check_permissions(ctx.message, manage_messages=True):
            raise commands.errors.CheckFailure('Cannot edit')

        self.replacements[p1] = [p2, author_id]
        return formatter.ok()

    def rm(self, pattern, author_id=''):
        #pattern = re.sub('^(`)?\\(\\?[^\\)]*\\)', '\\1', pattern)
        pattern = formatter.escape_mentions(pattern)

        #ensure that replace was found before proceeding
        if re.search('^`.*`$', pattern) and pattern[1:-1] in self.replacements:
            pattern = pattern[1:-1]
        elif pattern not in self.replacements:
            return formatter.error('Regex not in replacements.')

        #check if they have correct permissions
        if author_id != self.replacements[pattern][1] \
           and not perms.is_owner_check(author_id):
            raise commands.errors.CheckFailure('Cannot delete')

        self.replacements.pop(pattern)
        self.replacements.save()
        return formatter.ok()

    def ls(self):
        msg = '\n'.join(f'{k} -> {v}' for k, v in self.replacements.items())
        return formatter.code(msg)

    def replace(self, message):
        rep = message
        for i in self.replacements:
            rep = re.sub(r'(?i)\b{}\b'.format(i), self.replacements[i][0], rep)

        if rep.lower() != message.lower():
            return rep
        return None

    def is_banned(self, author_id):
        return author_id in self.permissions['rep-blacklist']
Пример #25
0
 def __init__(self, test=False):
     self.replacements = Config('configs/replace.json', save=(not test))
     self.permissions = Config('configs/perms.json', save=(not test))
     if 'rep-blacklist' not in self.permissions:
         self.permissions['rep-blacklist'] = []
Пример #26
0
class Osu:
    breatmap_sets_url_patterns = [re.compile('https?://osu.ppy.sh/s/(\\d+)')]
    breatmap_url_patterns = [re.compile('https?://osu.ppy.sh/b/(\\d+)')]
    user_url_patterns = [re.compile('https?://osu.ppy.sh/u/(\\d+)')]

    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.conf = Config('configs/osu.json')

        if 'api-key' not in self.conf:
            self.conf['api-key'] = input('enter osu api key: ')
        if 'watched-users' not in self.conf:
            self.conf['watched-users'] = {}
        self.conf.save()

        self.api = osuapi.OsuApi(self.conf['api-key'],
                                 connector=osuapi.AHConnector())

        self.loop.create_task(self.check_scores())

    async def osu_info(self, message):
        logger.debug('osu additional info helper start')

        if message.author.bot:
            logger.debug('  ignoring message (reason: bot)')
            return

        chan = message.channel

        for pattern in Osu.breatmap_sets_url_patterns:
            for bid in pattern.findall(message.content):
                beatmap = await self.api.get_beatmaps(beatmapset_id=bid)
                em = await self.osu_embed(beatmap)
                if not em:
                    chan.send('could not find beatmap')
                else:
                    await chan.send(embed=em)
            else:
                continue
            break

        for pattern in Osu.breatmap_url_patterns:
            for bid in pattern.findall(message.content):
                beatmap = await self.api.get_beatmaps(beatmap_id=bid)
                em = await self.osu_embed(beatmap[0])
                if not em:
                    chan.send('could not find beatmap')
                else:
                    await chan.send(embed=em)
            else:
                continue
            break

        for pattern in Osu.user_url_patterns:
            for uid in pattern.findall(message.content):
                user = await self.api.get_user(int(uid))
                em = await self.osu_embed(user[0])
                if not em:
                    chan.send('could not find user')
                else:
                    await chan.send(embed=em)
            else:
                continue
            break

        logger.debug('osu additional info helper end')

    @commands.group(name='osu', aliases=["o"])
    async def _osu(self, ctx):
        """
    manages osu stuff
    """
        if ctx.invoked_subcommand is None:
            await ctx.send(formatter.error("Please specify valid subcommand"))

    @_osu.command(name='recent', aliases=['r', 'last'])
    async def _recent(self, ctx, osu_username: str = ''):
        """
    shows last played map for a user
    if no user is specified, shows your last played item
    """
        auth = str(ctx.message.author.id)
        if osu_username:
            user = osu_username
        elif auth in self.conf['watched-users']:
            user = self.conf['watched-users'][auth]['uid']
        else:
            await ctx.send(
                'No user specified, nor are you linked to an OSU user')
            return

        user = await self.api.get_user(user)
        if not user:
            await ctx.send('Could not find user with matching name')
            return
        user = user[0]

        last = await self.api.get_user_recent(user.user_id, limit=50)
        last = [l for l in last if (not l.perfect) or (l.rank in 'SX')]
        if not last:
            await ctx.send(f'No recent play history for {user.username}')
            return

        em = await self.osu_embed(last[0])
        await ctx.send(embed=em)

    @_osu.command(name='disconnect', aliases=['u', 'unlink'])
    async def _osu_delink(self, ctx):
        """
    Unlinks a channel from watching an osu account
    """
        auth = str(ctx.message.author.id)
        chan = str(ctx.message.channel.id)

        if auth not in self.conf['watched-users']:
            await ctx.send("You arn't linked to an OSU account yet")
            return

        if chan not in self.conf['watched-users'][auth]['chans']:
            await ctx.send('This channel is not linked to your account')
            return

        self.conf['watched-users'][auth]['chans'].remove(chan)
        self.conf.save()

        await ctx.send(formatter.ok("Channel is now unlinked"))

    @_osu.command(name='connect', aliases=['c', 'link'])
    async def _osu_link(self, ctx, osu_username='', top_scores: int = 50):
        """
    Links a discord user account to an osu account
    osu_username - the username that you go by on https://osu.ppy.sh
    top_scores   - the top scores to report from latest [0, 100]
    """
        auth = str(ctx.message.author.id)
        chan = str(ctx.message.channel.id)

        user = self.conf['watched-users'].get(auth, {}).get('uid', None)
        user = await self.api.get_user(osu_username or user)
        if not user:
            await ctx.send('could not find user')
            return

        top_scores = max(0, min(100, top_scores))
        name = user[0].username
        user = user[0].user_id
        if top_scores:
            best = await self.api.get_user_best(user, limit=top_scores)
            best = [(i.beatmap_id, i.score) for i in best]
        else:
            best = []

        if auth in self.conf['watched-users']:
            self.conf['watched-users'][auth]['uid'] = user
            if chan not in self.conf['watched-users'][auth]['chans']:
                self.conf['watched-users'][auth]['chans'].append(chan)
            self.conf['watched-users'][auth]['last'] = best

        else:
            self.conf['watched-users'][auth] = {
                'uid': user,
                'num': top_scores,
                'chans': [chan],
                'last': best
            }
        self.conf.save()
        await ctx.send(formatter.ok(f'you are now linked to: {name}'))

    async def check_scores(self):
        while self == self.bot.get_cog('Osu'):
            try:
                for duid in self.conf['watched-users']:
                    ouid = self.conf['watched-users'][duid]['uid']
                    num = self.conf['watched-users'][duid]['num']
                    chans = self.conf['watched-users'][duid]['chans']
                    last = self.conf['watched-users'][duid]['last']
                    name = await self.api.get_user(ouid)
                    name = name[0].username
                    best = await self.api.get_user_best(ouid, limit=num)

                    for i, old, new in itertools.zip_longest(
                            range(num), last, best):
                        if not new:
                            break
                        elif not old or new.beatmap_id != old[
                                0] or new.score > old[1]:
                            em = await self.osu_embed(new)
                            em.title = f'New best #{i+1} for {name} - {em.title}'
                            for chan_id in chans:
                                try:
                                    chan = self.bot.get_channel(chan_id)
                                    await chan.send(embed=em)
                                except:
                                    logger.exception('issue with send %s',
                                                     str(em.to_dict()))
                            break
                    else:
                        continue
                    best = [(i.beatmap_id, i.score) for i in best]
                    self.conf['watched-users'][duid]['last'] = best
                    self.conf.save()
                await asyncio.sleep(30)
            except:
                #logger.exception("osu cog couldn't connect it seems")
                pass

    async def osu_embed(self, osu_obj):
        em = Embed()

        if type(osu_obj) == osuapi.model.Beatmap:
            length = osu_obj.total_length
            length = f'{length//3600:02}:{length//60:02}:{length%60:02}'
            diff = f'{osu_obj.difficultyrating:.2}'

            em.title = osu_obj.title
            em.url = f'https://osu.ppy.sh/b/{osu_obj.beatmap_id}'
            em.add_field(name='Artist', value=osu_obj.artist)
            em.add_field(name='Creator', value=osu_obj.creator)
            em.add_field(name='Difficulty', value=diff)
            em.add_field(name='BPM', value=str(osu_obj.bpm))
            em.add_field(name='Source', value=osu_obj.source)
            em.add_field(name='Max Combo', value=str(osu_obj.max_combo))
            em.add_field(name='Length', value=length)

        elif type(osu_obj) == list:
            if len(osu_obj) == 0:
                return None
            diff = ', '.join([f'{i.difficultyrating:.2}' for i in osu_obj])
            em = await self.osu_embed(osu_obj[0])
            em.url = f'https://osu.ppy.sh/s/{osu_obj[0].beatmapset_id}'
            em.set_field_at(2, name='Difficulty', value=diff)
            em.remove_field(3)  # remove BPM
            em.remove_field(4)  # remove Max Combo
            return em

        elif type(osu_obj) == osuapi.model.User:
            rank = '#{0.pp_rank} ({0.country} #{0.pp_country_rank})'.format(
                osu_obj)
            level = int(osu_obj.level)
            nextl = osu_obj.level * 100 % 100

            em.title = osu_obj.username
            em.url = f'https://osu.ppy.sh/u/{osu_obj.user_id}'
            em.add_field(name='Rank', value=rank)
            em.add_field(name='Accuracy', value=f'{osu_obj.accuracy:02.4}%')
            em.add_field(name='Level', value=f'{level} ({nextl:02.4}%)')
            em.add_field(name='Total PP', value=str(osu_obj.pp_raw))
            em.add_field(name='Play Count', value=str(osu_obj.playcount))

        elif type(osu_obj) in (osuapi.model.SoloScore,
                               osuapi.model.RecentScore):
            beatmap = await self.api.get_beatmaps(beatmap_id=osu_obj.beatmap_id
                                                  )
            beatmap = beatmap[0]
            em = await self.osu_embed(beatmap)
            rank = osu_obj.rank.replace('X', 'SS')
            if type(osu_obj) == osuapi.model.SoloScore:
                score = f'{osu_obj.score:,} ({rank} - {osu_obj.pp}pp)'
            else:
                score = f'{osu_obj.score:,} ({rank})'
            combo = f'{osu_obj.maxcombo}/{beatmap.max_combo}'

            em.add_field(name='Score', value=score)
            em.add_field(name='Combo', value=combo)
            em.remove_field(5)  # remove Max Combo

        if not em.thumbnail.url:
            em.set_thumbnail(url=await self.get_thumb_url(osu_obj))
        return em

    async def get_thumb_url(self, osu_obj):
        if type(osu_obj) == osuapi.model.Beatmap:
            if not hasattr(osu_obj,
                           'beatmapset_id') or not osu_obj.beatmapset_id:
                osu_obj = await self.api.get_beatmaps(
                    beatmap_id=osu_obj.beatmap_id)
                osu_obj = osu_obj[0]
            return f'https://b.ppy.sh/thumb/{osu_obj.beatmapset_id}l.jpg'
        elif type(osu_obj) == osuapi.model.User:
            return f'https://a.ppy.sh/{osu_obj.user_id}?.png'
        return 'http://w.ppy.sh/c/c9/Logo.png'

    def __unload(self):
        self.api.close()
Пример #27
0
import asyncio
import aiohttp
import datetime
import re, sys, os
import traceback

from cogs import *
from includes.utils.config import Config
import includes.utils.format as formatter

if not os.path.exists('configs'):
  makedirs('configs')
if not os.path.exists('logs'):
  makedirs('logs')

auth = Config('configs/auth.json')

import logging
from logging.handlers import TimedRotatingFileHandler
current_path = os.path.dirname(os.path.realpath(__file__))
logger = logging.getLogger('navi')
logger.setLevel(logging.INFO)
logformat = logging.Formatter(
  '%(asctime)s:%(name)s:%(levelname)s' + \
  '(%(pathname)s:%(lineno)s) - %(message)s'
)

#to log errors
path = os.path.join(current_path, 'logs/error.log')
error_log = logging.FileHandler(path)
error_log.setLevel(logging.ERROR)
Пример #28
0
class GroupMe:
    def __init__(self, bot):
        self.conf = Config('configs/groupme.json')
        self.bot = bot
        self.loop = bot.loop
        self.l_bots = []
        self.g_bots = {}
        self.g_groups = {}
        self.d_chans = {}

        if 'g_old' not in self.conf:
            self.conf['g_old'] = {}
        if 'links' not in self.conf:
            self.conf['links'] = {}
        if 'key' not in self.conf or not self.conf['key']:
            self.conf['key'] = input(
                'Please enter your GroupMe api key: ').strip()

        if not self.conf['key']:
            raise RuntimeError('No groupme key provied')

        self.conf.save()

        groupy.config.API_KEY = self.conf['key']

        for discord_chan_id in self.conf['links']:
            for g_id in self.conf['links'][discord_chan_id]:
                group, g_bot = self.get_group_bot(g_id)

                #print('linked discord({}) to groupme({})'.format(discord_chan_id,g_id))

                if not group:
                    #print('could not find group')
                    continue

                channel = self.bot.get_channel(discord_chan_id)
                if not channel:
                    #print('error chan')
                    continue

                if g_id not in self.g_groups:
                    #print('new g_groups: {} -> {}'.format(g_id, str(group)))
                    self.l_bots.append(g_bot)
                    self.g_groups[g_id] = group

                if str(channel.id) in self.g_bots:
                    self.g_bots[str(channel.id)].append(g_bot)
                    #print('append g_bots: {}'.format(str(self.g_bots)))
                else:
                    self.g_bots[str(channel.id)] = [g_bot]
                    #print('new g_bots: {}'.format(str(self.g_bots)))

                if g_id in self.d_chans:
                    self.d_chans[g_id].append(channel)
                else:
                    self.d_chans[g_id] = [channel]

        self.loop.create_task(self.poll())

    @commands.command()
    async def add_groupme_link(self, ctx, g_id: str):
        channel = ctx.message.channel
        group, g_bot = self.get_group_bot(g_id)

        if not group:
            await ctx.send(formatter.error("I am not in a group with that id"))
            return

        if g_id not in self.g_groups:
            self.l_bots.append(g_bot)
            self.g_groups[g_id] = group

        if str(channel.id) in self.g_bots:
            self.g_bots[str(channel.id)].append(g_bot)
            self.conf['links'][str(channel.id)].append(g_id)
        else:
            self.g_bots[str(channel.id)] = [g_bot]
            self.conf['links'][str(channel.id)] = [g_id]

        if g_id in self.d_chans:
            self.d_chans[g_id].append(channel)
        else:
            self.d_chans[g_id] = [channel]

        if g_id not in self.g_groups:
            self.conf['g_old'][g_id] = None

        self.conf.save()

        await ctx.send(formatter.ok())

    async def link_from_discord(self, message):
        logger.debug('groupme bridge start')

        if message.author.bot:
            logger.debug('  ignoring message (reason: bot)')
            return

        if message.content.startswith('.add_groupme_link'):
            logger.debug('  ignoring message (reason: link command)')
            return

        try:
            g_bots = self.g_bots[str(message.channel.id)]
            text = u'<\u200b{}> {}'.format(message.author.name,
                                           message.content)
            for a in message.attachments:
                text += '\n{}'.format(
                    str(a))  #TODO - I *think* attachments are strs
            for g_bot in g_bots:
                await self.loop.run_in_executor(None, g_bot.post, text)
        except:
            #print(self.g_bots)
            pass

        logger.debug('groupme bridge end')

    async def link_from_groupme(self, message, channels):
        try:
            #print('      send g->d - get text')
            text = message.text if message.text else ''

            name_hash = hashlib.md5()
            name_hash.update(str(message.name).strip().encode())
            name_hash = int(name_hash.hexdigest(), 16)
            #print('      send g->d - get color (\"{}\" -> {} % {} = {:02X})'.format(\
            #         str(message.name).strip(),
            #         name_hash,
            #         len(colours),
            #         colours[name_hash % len(colours)]
            #))
            c = colours[name_hash % len(colours)]

            #print('      send g->d - create embed')
            em = Embed(colour=c)

            #print('      send g->d - get attach')
            for a in message.attachments:
                #print('        attach process')
                if type(a) == groupy.object.attachments.Location:
                    text += '\n[{} - ({}, {})]'.format(a.name, a.lat, a.lng)
                elif type(a) == groupy.object.attachments.Image:
                    #print('        image: {}'.format(str(a.url)))
                    for channel in channels:
                        await channel.send('<{}> {}'.format(
                            str(message.name), a.url))
                    #em.set_image(str(a.url))
                elif type(a) == groupy.object.attachments.Mentions:
                    pass  #TODO at some point?
                elif type(a) == groupy.object.attachments.Emoji:
                    pass  #TODO maybe when their doc explain how this works

            #print('      send g->d - set author: {} [{}]'.format(str(message.name),
            #                                               str(message.avatar_url)
            #))
            if text:
                if message.avatar_url:
                    em.set_author(name=str(message.name),
                                  icon_url=str(message.avatar_url))
                else:
                    em.set_author(name=str(message.name))

                em.description = text

                #print('      send g->d - send embed to channel(s)')
                for channel in channels:
                    #print('        sending {} to {}'.format(str(em), str(channel)))
                    await channel.send(embed=em)
                #print('      send g->d - all ok')
        except Error as err:
            #print(err)
            pass

    async def poll(self):
        #print('polling')
        while self == self.bot.get_cog('GroupMe'):
            messages = []
            for bot in self.l_bots:
                #print('  group: {}'.format(str(self.g_groups[bot.group_id])))
                messages = []
                channels = self.d_chans[bot.group_id]

                try:
                    #print('    p refresh')
                    self.g_groups[bot.group_id].refresh()
                    all_messages = self.g_groups[bot.group_id].messages()

                    #print('    p splice')
                    for message in all_messages:
                        #print('      check 1')
                        if str(message.id) == self.conf['g_old'][bot.group_id]:
                            break
                        #print('      check 2')
                        if not message.text or not message.text.startswith(
                                u'<\u200b'):
                            messages.append(message)

                    #print('    p save progress')
                    if len(all_messages) > 0:
                        self.conf['g_old'][bot.group_id] = str(
                            all_messages.newest.id)
                        self.conf.save()

                    #print('    p send')
                    for message in reversed(messages):
                        await self.link_from_groupme(message, channels)
                except:
                    #print('    polling failed')
                    pass

            #print('    p wait')
            await asyncio.sleep(5 if messages else 25)
            #print('    p queue')

    def get_group_bot(self, g_id):
        group = None
        g_bot = None

        for g in groupy.Group.list():
            if str(g.id) == str(g_id):
                group = g
                #break

        if not group:
            return None, None

        for bot in groupy.Bot.list():
            if str(bot.group_id) == str(g_id):
                g_bot = bot
                break

        if not g_bot:
            g_bot = groupy.Bot.create('Navi',
                                      group,
                                      avatar_url=self.bot.user.avatar_url)
        return group, g_bot
Пример #29
0
class AZ:
    def __init__(self, test=False):
        self.last = {}
        self.conf = Config('configs/az.json', save=(not test))
        if 'lenny' not in self.conf:
            self.conf['lenny'] = {}
        if 'img-reps' not in self.conf:
            self.conf['img-reps'] = {}
        if 'repeat_after' not in self.conf:
            self.conf['repeat_after'] = 3
        self.conf.save()

    def lenny(self, first=''):
        out = None
        try:
            num = int(first)
            if num < 1:
                num = 1
            if num > 10:
                num = 10
        except:
            num = 1
            out = self.conf['lenny'].get(first.lower(), None)
        out = formatter.code(out) if out else '\n( ͡° ͜ʖ ͡° )'
        return out * num

    def shrug(self):
        return '¯\_(ツ)_/¯'

    @staticmethod
    def renderLatex(text,
                    fntsz=12,
                    dpi=300,
                    fsz=.01,
                    fmt='svg',
                    file=None,
                    **kargs):
        if type(file) == str and file:
            if not file.endswith(fmt):
                file += '.' + fmt
            with open(file, 'w') as f:
                return renderLatex(text, fntsz, dpi, fsz, fmt, f, **kargs)
        text = text.strip().replace('\n', '\\\\')
        if text.startswith('\\begin'):
            text = f'\\[{text}\\]'
        elif not text.startswith('$') and not text.startswith('\\['):
            text = f'\\[\\begin{{split}}{text}\\end{{split}}\\]'
        logger.debug(f'attempting to render latex string: \"{text}\"')

        plt.rc('text', usetex=True)
        plt.rcParams['text.latex.preamble'] = [
            r'\usepackage{amsmath}',
            r'\usepackage{amssymb}',
            r'\usepackage{tikz}',
            r'\usepackage{xcolor}',
            r'\usepackage[mathscr]{euscript}',
            r'\usepackage{mathrsfs}',
        ]

        fig = plt.figure(figsize=(fsz, fsz))
        fig.text(0,
                 0,
                 text,
                 fontsize=fntsz,
                 ha='center',
                 ma='center',
                 linespacing=1,
                 **kargs)

        output = BytesIO() if file is None else file
        fig.savefig(output,
                    dpi=dpi,
                    transparent=True,
                    format=fmt,
                    bbox_inches='tight',
                    pad_inches=0.1,
                    frameon=False)
        plt.close(fig)

        if file is None:
            output.seek(0)
            return output

    def get_colour(self, colour):
        colour = colour.lower().strip()
        match = re.search('^(0[hx])?([a-f0-9]{6})$', colour)
        if colour in dh.colours:
            c = dh.colours[colour]
        elif match:
            c = discord.Colour(int(match.group(2), 16))
        else:
            return None
        return c

    def img(self, *search):
        if not os.path.exists(self.conf.get('path', '')):
            logger.debug('could not find images')
            raise IOError('No images found')

        #try:
        #  git_sync(self.conf.get('path'))
        #except:
        #  pass

        logger.debug('input: %s', ', '.join(search))
        search = [re.sub(r'[^\w\./#\* -]+', '', i).lower() for i in search]
        search = dh.remove_comments(search)
        logger.debug('uncomment: %s', ', '.join(search))

        try:
            path = azfind.search(self.conf['path'], search)
        except:
            path = ''

        if not path.strip():
            return None, None

        # fix line apngs a bit (they only loop once)
        loop_apng(path)

        try:
            logger.info(path)
            if self.conf.get('path-rep'):
                url = path.replace(self.conf['path'], self.conf['path-rep'])
            else:
                url = 'No url, `pat-rep` not set'
            return path, url
        except:
            raise

    async def censor(self, bot, message):
        reps = self.conf.get('censor', {})
        cont = message.content
        chan = message.channel
        auth = message.author.mention
        send = False

        for pat, rep in reps.items():
            if re.search(pat, cont):
                send = True
                cont = re.sub(pat, rep, cont)

        if send:
            await message.delete()
            await chan.send(f'<{auth}> {cont}')

    async def repeat(self, bot, message):
        chan = message.channel
        data = self.last.get(chan, ['', 0])

        if not message.content:
            return

        if data[0] == message.content.lower():
            data[1] += 1
        else:
            data = [message.content.lower(), 1]

        if data[1] == self.conf.get('repeat_after', 3):
            await chan.send(message.content)
            data[1] = 0

        self.last[chan] = data
Пример #30
0
class General:
    def __init__(self, bot):
        self.bot = bot
        self.loop = bot.loop
        self.stopwatches = {}
        self.conf = Config('configs/general.json')

        heap = self.bot.get_cog('HeapCog')

        if 'responses' not in self.conf:
            self.conf['responses'] = {}
        if 'todo' not in self.conf:
            self.conf['todo'] = {}
        if 'situations' not in self.conf:
            self.conf['situations'] = []
        if '8-ball' not in self.conf:
            self.conf['8-ball'] = []
        for rem in self.conf.pop('reminders', []):
            self.loop.run_until_complete(heap.push(rem, None))
        self.conf.save()

    @commands.command(hidden=True)
    async def ping(self, ctx):
        """Pong."""
        async with ctx.typing():
            await ctx.send("Pong.")

    @commands.command()
    async def time(self, ctx, first=''):
        '''remind people to hurry up'''
        say = lambda msg: ctx.send(msg)
        if random.randrange(50) or not first.startswith('@'):
            async with ctx.typing():
                now = datetime.now().replace(microsecond=0)
                await say(now.isoformat().replace('T', ' '))
        else:
            async with ctx.typing():
                await asyncio.sleep(1.2)
                await say('ゲネラルリベラル')
            async with ctx.typing():
                await asyncio.sleep(0.4)
                await say('デフレイスパイラル')
            async with ctx.typing():
                await asyncio.sleep(0.5)
                await say('ナチュラルミネラル')
            async with ctx.typing():
                await asyncio.sleep(0.2)
                await say('さあお出で' + (': ' + first if first else ''))

    @commands.command()
    async def invite(self, ctx):
        '''reply with a link that allows this bot to be invited'''
        await ctx.send(
            f'https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}'
            + '&permissions=305260592&scope=bot')

    async def tally(self, message):
        chan = message.channel
        user = message.author
        mess = message.content
        loop = asyncio.get_event_loop()

        logger.debug('tally start')
        #bots don't get a vote
        if user.bot:
            return

        if len(mess.strip()) < 2 or \
            mess.strip()[0] in self.bot.command_prefix + ['$','?','!']:
            return

        test_poll = Poll('', [], chan, 0, 1)

        heap = self.bot.get_cog('HeapCog')
        for poll in heap:
            if test_poll == poll:
                await loop.run_in_executor(None, poll.vote, user, mess)

        logger.debug('tally end')

    async def respond(self, message):
        if message.author.bot:
            return

        if len(message.content.strip()) < 2 or \
            message.content.strip()[0] in self.bot.command_prefix + ['$','?','!']:
            return

        logger.debug('respond start')
        loop = asyncio.get_event_loop()

        for i in self.conf['responses']:
            if re.search("(?i){}".format(i[0]), message.content):
                rep = i[1]
                subs = {
                    "\\{un\\}": message.author.name,
                    "\\{um\\}": message.author.mention,
                    "\\{ui\\}": message.author.mention,
                    "\\{situations\\}": random.choice(self.conf['situations'])
                }
                for j in re.findall("\\(.*?\\|.*?\\)", rep):
                    rep = rep.replace(j, random.choice(j[1:-1].split("|")))
                for j in subs:
                    rep = await loop.run_in_executor(None, re.sub, j, subs[j],
                                                     rep)
                for j in re.findall("\\(.*?\\|.*?\\)", rep):
                    rep = rep.replace(j, random.choice(j[1:-1].split("|")))
                msg = re.sub("(?i){}".format(i[0]), rep, message.content)
                if rep:
                    await message.channel.send(msg)
                break
        logger.debug('respond start')

    @commands.command(name='roll', aliases=['r', 'clench'])
    async def _roll(self, ctx, *dice):
        """rolls dice given pattern [Nd]S[(+|-)C]

    N: number of dice to roll
    S: side on the dice
    C: constant to add or subtract from each die roll
    """
        loop = asyncio.get_event_loop()

        async with ctx.typing():
            total, roll = await loop.run_in_executor(None, self.rolls, dice)
            roll = '\n'.join(roll)
            message = ctx.message.author.mention + ':\n'
            if '\n' in roll:
                message += code(roll + f'\nTotal: {total}')
            else:
                message += inline(roll)
            await ctx.send(message)

    @commands.command(name="8ball", aliases=["8"])
    async def _8ball(self, ctx, *, question: str):
        """Ask 8 ball a question

    Question must end with a question mark.
    """
        async with ctx.typing():
            if question.endswith("?") and question != "?":
                await ctx.send("`" + random.choice(self.conf['8-ball']) + "`")
            else:
                await ctx.send("That doesn't look like a question.")

    @commands.group(aliases=['t', 'td'])
    async def todo(self, ctx):
        '''
    manages user TODO list
    Note: if no sub-command is specified, TODOs will be listed
    '''
        if ctx.invoked_subcommand is None:
            async with ctx.typing():
                await self._td_list(ctx)

    @todo.command(name='list', aliases=['l', 'ls'])
    async def _td_list_wp(self, ctx):
        '''
    prints your complete todo list
    '''
        async with ctx.typing():
            await self._td_list(ctx)

    @todo.command(name='add', aliases=['a', 'insert', 'i'])
    async def _td_add(self, ctx, *, task: str):
        '''
    adds a new task to your todo list
    '''
        async with ctx.typing():
            todos = self.conf['todo'].get(str(ctx.message.author.id), [])
            todos.append([False, task])
            self.conf['todo'][str(ctx.message.author.id)] = todos
            self.conf.save()
            await ctx.send(ok())

    @todo.command(name='done', aliases=['d', 'complete', 'c'])
    async def _td_done(self, ctx, *, index: int):
        '''
    sets/unsets a task as complete
    Note: indicies start at 1
    '''
        async with ctx.typing():
            todos = self.conf['todo'].get(str(ctx.message.author.id), [])
            if len(todos) < index or index <= 0:
                await ctx.send(error('Invalid index'))
            else:
                index -= 1
                todos[index][0] = not todos[index][0]
                self.conf['todo'][str(ctx.message.author.id)] = todos
                self.conf.save()
                await ctx.send(ok())

    @todo.command(name='remove', aliases=['rem', 'rm', 'r'])
    async def _td_remove(self, ctx, *, index: int):
        '''
    remove a task from your todo list
    Note: indicies start at 1
    '''
        async with ctx.typing():
            todos = self.conf['todo'].get(str(ctx.message.author.id), [])
            if len(todos) < index or index <= 0:
                await ctx.send(error('Invalid index'))
            else:
                task = todos.pop(index - 1)
                self.conf['todo'][str(ctx.message.author.id)] = todos
                self.conf.save()
                await ctx.send(ok('Removed task #{}'.format(index)))

    async def _td_list(self, ctx):
        todos = self.conf['todo'].get(str(ctx.message.author.id), [])
        if not todos:
            await ctx.send('No TODOs found.')
        else:
            #TODO - ensure that the outgoing message is not too long
            msg = 'TODO:\n'
            length = len(str(len(todos)))
            done = '{{:0{}}} - ~~{{}}~~\n'.format(length)
            working = '{{:0{}}} - {{}}\n'.format(length)
            for i, todo in enumerate(todos, 1):
                if todo[0]:
                    msg += done.format(i, todo[1])
                else:
                    msg += working.format(i, todo[1])
            await ctx.send(msg)

    @commands.group(aliases=["sw"])
    async def stopwatch(self, ctx):
        """
    manages user stopwatch
    starts/stops/unpauses (depending on context)
    """
        if ctx.invoked_subcommand is None:
            aid = str(ctx.message.author.id)
            if aid in self.stopwatches and self.stopwatches[aid][0]:
                await self._sw_stop(ctx)
            else:
                await self._sw_start(ctx)

    @stopwatch.command(name='start', aliases=['unpause', 'u', 'resume', 'r'])
    async def _sw_start_wrap(self, ctx):
        """
    unpauses or creates new stopwatch
    """
        await self._sw_start(ctx)

    async def _sw_start(self, ctx):
        aid = str(ctx.message.author.id)
        tme = ctx.message.timestamp.timestamp()
        if aid in self.stopwatches and self.stopwatches[aid][0]:
            await ctx.send('You\'ve already started a stopwatch.')
        elif aid in self.stopwatches:
            self.stopwatches[aid][0] = tme
            await ctx.send('Stopwatch resumed.')
        else:
            self.stopwatches[aid] = [tme, 0]
            await ctx.send('Stopwatch started.')

    @stopwatch.command(name='stop', aliases=['end', 'e'])
    async def _sw_stop_wrap(self, ctx):
        """
    prints time and deletes timer

    works even if paused
    """
        await self._sw_stop(ctx)

    async def _sw_stop(self, ctx):
        aid = str(ctx.message.author.id)
        now = ctx.message.timestamp.timestamp()
        old = self.stopwatches.pop(aid, None)
        if old:
            if old[0]:
                tme = now - old[0] + old[1]
            else:
                tme = old[1]
            tme = str(timedelta(seconds=tme))
            msg = '```Stopwatch stopped: {}\n'.format(tme)
            for lap in zip(range(1, len(old)), old[2:]):
                msg += '\nLap {0:03} - {1}'.format(*lap)
            msg += '```'
            await ctx.send(msg)
        else:
            await ctx.send('No stop watches started, cannot stop.')

    @stopwatch.command(name='status', aliases=['look', 'peak'])
    async def _sw_status(self, ctx):
        aid = str(ctx.message.author.id)
        now = ctx.message.timestamp.timestamp()
        if aid in self.stopwatches:
            old = self.stopwatches[aid]
            if old[0]:
                tme = now - old[0] + old[1]
            else:
                tme = old[1]
            tme = str(timedelta(seconds=tme))
            msg = '```Stopwatch time: {}'.format(tme)
            if old[0]:
                msg += '\n'
            else:
                msg += ' [paused]\n'
            for lap in zip(range(1, len(old)), old[2:]):
                msg += '\nLap {0:03} - {1}'.format(*lap)
            msg += '```'
            await ctx.send(msg)
        else:
            await ctx.send('No stop watches started, cannot look.')

    @stopwatch.command(name='lap', aliases=['l'])
    async def _sw_lap(self, ctx):
        """
    prints time

    does not pause, does not resume, does not delete
    """
        aid = str(ctx.message.author.id)
        now = ctx.message.timestamp.timestamp()
        if aid in self.stopwatches:
            old = self.stopwatches[aid]
            if old[0]:
                tme = now - old[0] + old[1]
            else:
                tme = old[1]
            tme = str(timedelta(seconds=tme))
            await ctx.send("Lap #{:03} time: **{}**".format(len(old) - 1, tme))
            if self.stopwatches[aid][-1] != tme:
                self.stopwatches[aid].append(tme)
        else:
            await ctx.send('No stop watches started, cannot lap.')

    @stopwatch.command(name='pause', aliases=['p', 'hold', 'h'])
    async def _sw_pause(self, ctx):
        """
    pauses the stopwatch

    Also prints current time, does not delete
    """
        aid = str(ctx.message.author.id)
        now = ctx.message.timestamp.timestamp()
        if aid in self.stopwatches and self.stopwatches[aid][0]:
            old = now - self.stopwatches[aid][0] + self.stopwatches[aid][1]
            self.stopwatches[aid] = [0, old]
            old = str(timedelta(seconds=old))
            await ctx.send("Stopwatch paused: **{}**".format(old))
        elif aid in self.stopwatches:
            await ctx.send('Stop watch already paused.')
        else:
            await ctx.send('No stop watches started, cannot pause.')

    def rolls(self, dice):
        out = []

        if not dice:
            dice = ['20']

        gobal_total = 0
        for roll in dice:
            match = re.search('^((\\d+)?d)?(\\d+)([+-]\\d+)?$', roll, re.I)
            message = ''
            total = 0
            if not match:
                message = 'Invalid roll'
            else:
                times = 1
                sides = int(match.group(3))
                add = 0
                if match.group(2):
                    times = int(match.group(2))
                if match.group(4):
                    add = int(match.group(4))

                if times > 100:
                    message = 'Cannot roll that many dice'
                elif sides > 120:
                    message = 'Cannot find a dice with that many sides'
                elif times < 1:
                    message = 'How?'
                elif sides < 2:
                    message = 'No'
                else:
                    for i in range(times):
                        num = random.randint(1, sides) + add
                        total += num
                        message += '{}, '.format(num)
                    message = message[:-2]
                    gobal_total += total
                    if times > 1:
                        message += ' (sum = {})'.format(total)
            out.append('{}: {}'.format(roll, message))
        return (gobal_total, out)

    @commands.command(aliases=['c', 'choice'])
    async def choose(self, ctx, *, choices):
        """Chooses a value from a comma seperated list"""
        choices = split(choices)
        choice = random.choice(choices)
        choice_reps = {
            r'(?i)^(should)\s+I\s+': r'You \1 ',
            r'(?i)^([wcs]hould|can|are|were|is)\s+(\S+)\s+': r'\2 \1 ',
            r'\?$': '.',
            r'(?i)^am\s+I\s+': 'Thou art ',
            r'(?i)\b(I|me)\b': 'you',
            r'(?i)\bmy\b': 'your'
        }
        for r in choice_reps:
            choice = re.sub(r, choice_reps[r], choice)

        message = ctx.message.author.mention + ':\n'
        message += inline(choice)
        await ctx.send(message)

    @commands.command(name='remindme', aliases=['remind'])
    async def _add_reminder(self, ctx, *, message: str):
        '''
    adds a reminder

    'at' can be used when specifing exact time
    'in' is optional for offsets
    'me' can be seperate or part of the command name (also optinal)
    cannot mix offsets and exact times

    Samples:
    .remind me in 5 h message
    .remind me in 5 hours 3 m message
    .remind me 1 week message
    .remind me 7 months message
    .remindme in 7 months message
    .remind me at 2017-10-23 message
    .remind me at 2017-10-23T05:11:56 message
    .remindme at 2017-10-23 05:11:56 message
    .remindme at 10/23/2017 5:11 PM message
    .remind at 7:11 message
    .remind at 7:11:15 message
    .remind [me] remove <id>
    .remind [me] end <id>
    '''
        heap = self.bot.get_cog('HeapCog')
        author = str(ctx.message.author.id)
        channel = str(ctx.message.channel.id)
        match = re.match(r'(?i)^(me\s+)?(remove|end|stop)\s+(\d+)', message)

        if match:
            async with ctx.typing():
                rid = int(match.group(3))
                for index, item in enumerate(heap):
                    if type(item) == Reminder \
                        and item.reminder_id == rid \
                        and item.user_id == author:
                        heap.pop(index)
                        await ctx.send(
                            ok(f'Message with id {rid} has been removed'))
                        return
                else:
                    await ctx.send(ok(f'Could not find message with id {rid}'))
        else:
            r = Reminder(channel, author, message)
            await heap.push(r, ctx)

    @commands.command(aliases=['a', 'ask'])
    async def question(self, ctx):
        '''Answers a question with yes/no'''
        message = ctx.message.author.mention + ':\n'
        message += inline(random.choice(['yes', 'no']))
        await ctx.send(message)

    @commands.command()
    async def poll(self, ctx, *, question):
        '''
    Starts a poll
    format:
    poll question? opt1, opt2, opt3 or opt4...
    poll stop|end
    '''
        heap = self.bot.get_cog('HeapCog')
        cid = int(ctx.message.channel.id)

        if question.lower().strip() in ['end', 'stop']:
            for index, poll in enumerate(heap):
                if isinstance(poll, Poll) and poll.channel_id == cid:
                    heap.pop(index)
                    await poll.end(self.bot)
                    break
            else:
                await ctx.send('There is no poll active in this channel')
            return

        match = re.search(r'^(.*?\?)\s*(.*?)$', question)
        if not match:
            await ctx.send('Question could not be found.')
            return

        options = split(match.group(2))
        question = escape_mentions(match.group(1))

        poll = Poll(question, options, ctx.message.channel, 600)

        for item in heap:
            if poll == item:
                await ctx.send('There is a poll active in this channel already'
                               )
                return
        await heap.push(poll, ctx)