text: ﻿import discord
  from discord.ext import commands
  import asyncio
  import aiohttp
  import random
  from datetime import datetime, timedelta
  import math
  import json
  
  # #####################################################################################
  # math parser used by $calc
  class MathParser:
      def __init__(self):
          self.expression = ""
          self.index = 0
          self.vars = {}
          self.funcs = ['cos', 'sin', 'tan', 'acos', 'asin', 'atan', 'cosh', 'sinh', 'tanh', 'acosh', 'asinh', 'atanh', 'exp', 'ceil', 'abs', 'factorial', 'floor', 'round', 'trunc', 'log', 'log2', 'log10', 'sqrt', 'rad', 'deg']
  
      def evaluate(self, expression = "", vars={}):
          self.expression = expression.replace(' ', '').replace('\t', '').replace('\n', '').replace('\r', '')
          self.index = 0
          self.vars = {
              'pi' : 3.141592653589793,
              'e' : 2.718281828459045
          }
          self.vars = {**self.vars, **vars}
          for func in self.funcs:
              if func in self.vars: raise Exception("Variable name '{}' can't be used".format(func))
          value = float(self.parse())
          if self.isNotDone(): raise Exception("Unexpected character '{}' found at index {}".format(self.peek(), self.index))
          epsilon = 0.0000000001
          if int(value) == value: return int(value)
          elif int(value + epsilon) != int(value):
              return int(value + epsilon)
          elif int(value - epsilon) != int(value):
              return int(value)
          return value
  
      def isNotDone(self):
          return self.index < len(self.expression)
  
      def peek(self):
          return self.expression[self.index:self.index + 1]
  
      def parse(self):
          values = [self.multiply()]
          while True:
              c = self.peek()
              if c in ['+', '-']:
                  self.index += 1
                  if c == '-': values.append(- self.multiply())
                  else: values.append(self.multiply())
              else:
                  break
          return sum(values)
  
      def multiply(self):
          values = [self.parenthesis()]
          while True:
              c = self.peek()
              if c in ['*', 'x']:
                  self.index += 1
                  values.append(self.parenthesis())
              elif c in ['/', '%']:
                  div_index = self.index
                  self.index += 1
                  denominator = self.parenthesis()
                  if denominator == 0:
                      raise Exception("Division by 0 occured at index {}".format(div_index))
                  if c == '/': values.append(1.0 / denominator)
                  else: values.append(1.0 % denominator)
              elif c == '^':
                  self.index += 1
                  exponent = self.parenthesis()
                  values[-1] = values[-1] ** exponent
              elif c == '!':
                  self.index += 1
                  values[-1] = math.factorial(values[-1])
              else:
                  break
          value = 1.0
          for factor in values: value *= factor
          return value
  
      def parenthesis(self):
          if self.peek() == '(':
              self.index += 1
              value = self.parse()
              if self.peek() != ')': raise Exception("No closing parenthesis found at character {}".format(self.index))
              self.index += 1
              return value
          else:
              return self.negative()
  
      def negative(self):
          if self.peek() == '-':
              self.index += 1
              return -1 * self.parenthesis()
          else:
              return self.value()
      
      def value(self):
          if self.peek() in '0123456789.':
              return self.number()
          else:
              return self.variable_or_function()
  
      def variable_or_function(self):
          var = ''
          while self.isNotDone():
              c = self.peek()
              if c.lower() in '_abcdefghijklmnopqrstuvwxyz0123456789':
                  var += c
                  self.index += 1
              else:
                  break
          
          value = self.vars.get(var, None)
          if value == None:
              if var not in self.funcs: raise Exception("Unrecognized variable '{}'".format(var))
              else:
                  param = self.parenthesis()
                  if var == 'cos': value = math.cos(param)
                  elif var == 'sin': value = math.sin(param)
                  elif var == 'tan': value = math.tan(param)
                  elif var == 'acos': value = math.acos(param)
                  elif var == 'asin': value = math.asin(param)
                  elif var == 'atan': value = math.atan(param)
                  elif var == 'cosh': value = math.cosh(param)
                  elif var == 'sinh': value = math.sinh(param)
                  elif var == 'tanh': value = math.tanh(param)
                  elif var == 'acosh': value = math.acosh(param)
                  elif var == 'asinh': value = math.asinh(param)
                  elif var == 'atanh': value = math.atanh(param)
                  elif var == 'exp': value = math.exp(param)
                  elif var == 'ceil': value = math.ceil(param)
                  elif var == 'floor': value = math.floor(param)
                  elif var == 'round': value = math.floor(param)
                  elif var == 'factorial': value = math.factorial(param)
                  elif var == 'abs': value = math.fabs(param)
                  elif var == 'trunc': value = math.trunc(param)
                  elif var == 'log':
                      if param <= 0: raise Exception("Can't evaluate the logarithm of '{}'".format(param))
                      value = math.log(param)
                  elif var == 'log2':
                      if param <= 0: raise Exception("Can't evaluate the logarithm of '{}'".format(param))
                      value = math.log2(param)
                  elif var == 'log10':
                      if param <= 0: raise Exception("Can't evaluate the logarithm of '{}'".format(param))
                      value = math.log10(param)
                  elif var == 'sqrt': value = math.sqrt(param)
                  elif var == 'rad': value = math.radians(param)
                  elif var == 'deg': value = math.degrees(param)
                  else: raise Exception("Unrecognized function '{}'".format(var))
          return float(value)
  
      def number(self):
          strValue = ''
          decimal_found = False
          c = ''
          
          while self.isNotDone():
              c = self.peek()
              if c == '.':
                  if decimal_found:
                      raise Exception("Found an extra period in a number at character {}".format(self.index))
                  decimal_found = True
                  strValue += '.'
              elif c in '0123456789':
                  strValue += c
              else:
                  break
              self.index += 1
          
          if len(strValue) == 0:
              if c == '': raise Exception("Unexpected end found")
              else: raise Exception("A number was expected at character {} but instead '{}' was found".format(self.index, char))
          return float(strValue)
  
  # #####################################################################################
  # Cogs
  class General(commands.Cog):
      """General commands."""
      def __init__(self, bot):
          self.bot = bot
          self.color = 0x8fe3e8
  
      def startTasks(self):
          self.bot.runTask('reminder', self.remindertask)
  
      async def remindertask(self):
          while True:
              if self.bot.exit_flag: return
              try:
                  c = self.bot.getJST() + timedelta(seconds=30)
                  for r in list(self.bot.reminders.keys()):
                      di = 0
                      u = self.bot.get_user(int(r))
                      if u is None: continue
                      while di < len(self.bot.reminders[r]):
                          if c > self.bot.reminders[r][di][0]:
                              try:
                                  await u.send(embed=self.bot.buildEmbed(title="Reminder", description=self.bot.reminders[r][di][1]))
                              except Exception as e:
                                  await self.bot.sendError('remindertask', "User: {}\nReminder: {}\nError: {}".format(u.name, self.bot.reminders[r][di][1], e))
                              self.bot.reminders[r].pop(di)
                              self.bot.savePending = True
                          else:
                              di += 1
                      if len(self.bot.reminders[r]) == 0:
                          self.bot.reminders.pop(r)
                          self.bot.savePending = True
              except asyncio.CancelledError:
                  await self.bot.sendError('remindertask', 'cancelled')
                  return
              except Exception as e:
                  await self.bot.sendError('remindertask', str(e))
                  await asyncio.sleep(200)
              await asyncio.sleep(40)
  
      def isDisabled(): # for decorators
          async def predicate(ctx):
              return False
          return commands.check(predicate)
  
      def isAuthorized(): # for decorators
          async def predicate(ctx):
              return ctx.bot.isAuthorized(ctx)
          return commands.check(predicate)
  
      # get a 4chan thread
      async def get4chan(self, board : str, search : str): # be sure to not abuse it, you are not supposed to call the api more than once per second
          try:
              search = search.lower()
              url = 'http://a.4cdn.org/{}/catalog.json'.format(board) # board catalog url
              async with aiohttp.ClientSession() as session:
                  async with session.get(url) as r:
                      if r.status == 200:
                          data = await r.json()
              threads = []
              for p in data:
                  for t in p["threads"]:
                      try:
                          if t["sub"].lower().find(search) != -1 or t["com"].lower().find(search) != -1:
                              threads.append([t["no"], t["replies"]]) # store the thread ids matching our search word
                      except:
                          pass
              threads.sort(reverse=True)
              return threads
          except:
              return []
  
      @commands.command(no_pm=True, cooldown_after_parsing=True)
      @isAuthorized()
      async def roll(self, ctx, dice : str = ""):
          """Rolls a dice in NdN format."""
          try:
              rolls, limit = map(int, dice.split('d'))
              result = ", ".join(str(random.randint(1, limit)) for r in range(rolls))
              await ctx.send(embed=self.bot.buildEmbed(title="{}'s dice Roll(s)".format(ctx.message.author.display_name), description=result, color=self.color))
          except:
              await ctx.send(embed=self.bot.buildEmbed(title="Format has to be in NdN", footer="example: roll 2d6", color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['choice'])
      @isAuthorized()
      @commands.cooldown(2, 10, commands.BucketType.guild)
      async def choose(self, ctx, *, choices : str ):
          """Chooses between multiple choices.
          Use quotes if one of your choices contains spaces.
          Example: $choose I'm Alice ; Bob"""
          try:
              possible = choices.split(";")
              if len(possible) < 2: raise Exception()
              await ctx.send(embed=self.bot.buildEmbed(title="{}, I choose".format(ctx.message.author.display_name), description=random.choice(possible), color=self.color))
          except:
              await ctx.send(embed=self.bot.buildEmbed(title="Give me a list of something to choose from 😔, separated by ';'", color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['math'])
      @commands.cooldown(2, 10, commands.BucketType.guild)
      async def calc(self, ctx, *terms : str):
          """Process a mathematical expression
          You can define a variable by separating using a comma.
          Some functions are also available.
          Example: cos(a + b) / c, a = 1, b=2,c = 3"""
          try:
              m = " ".join(terms).split(",")
              d = {}
              for i in range(1, len(m)): # process the variables if any
                  x = m[i].replace(" ", "").split("=")
                  if len(x) == 2: d[x[0]] = float(x[1])
                  else: raise Exception('')
              msg = "{} = **{}**".format(m[0], MathParser().evaluate(m[0], d))
              if len(d) > 0:
                  msg += "\nwith:\n"
                  for k in d:
                      msg += "{} = {}\n".format(k, d[k])
              await ctx.send(embed=self.bot.buildEmbed(title="Calculator", description=msg, color=self.color))
          except Exception as e:
              await ctx.send(embed=self.bot.buildEmbed(title="Error", description=str(e), color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True)
      @commands.cooldown(1, 5, commands.BucketType.guild)
      async def jst(self, ctx):
          """Post the current time, JST timezone"""
          await ctx.send(embed=self.bot.buildEmbed(title="{} {:%Y/%m/%d %H:%M} JST".format(self.bot.getEmote('clock'), self.bot.getJST()), color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, alias=['inrole', 'rolestat'])
      @isAuthorized()
      async def roleStats(self, ctx, *name : str):
          """Search how many users have a matching role
          use quotes if your match contain spaces
          add 'exact' at the end to force an exact match"""
          g = ctx.author.guild
          i = 0
          if len(name) > 0 and name[-1] == "exact":
              exact = True
              name = name[:-1]
          else:
              exact = False
          name = ' '.join(name)
          for member in g.members:
              for r in member.roles:
                  if r.name == name or (exact == False and r.name.lower().find(name.lower()) != -1):
                      i += 1
          if exact != "exact":
              await ctx.send(embed=self.bot.buildEmbed(title="Roles containing: {}".format(name), description="{} user(s)".format(i), thumbnail=g.icon_url, footer="on server {}".format(g.name), color=self.color))
          else:
              await ctx.send(embed=self.bot.buildEmbed(title="Roles matching: {}".format(name), description="{} user(s)".format(i), thumbnail=g.icon_url, footer="on server {}".format(g.name), color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['hgg2d'])
      @commands.cooldown(1, 10, commands.BucketType.default)
      async def hgg(self, ctx):
          """Post the latest /hgg2d/ threads"""
          if not ctx.channel.is_nsfw():
              await ctx.send(embed=self.bot.buildEmbed(title=':underage: NSFW channels only'))
              return
          threads = await self.get4chan('vg', '/hgg2d/')
          if len(threads) > 0:
              msg = ""
              for t in threads:
                  msg += '🔞 https://boards.4channel.org/vg/thread/{} ▫️ *{} replies*\n'.format(t[0], t[1])
              await ctx.send(embed=self.bot.buildEmbed(title="/hgg2d/ latest thread(s)", description=msg, footer="Good fap, fellow 4channeler", color=self.color))
          else:
              await ctx.send(embed=self.bot.buildEmbed(title="/hgg2d/ Error", description="I couldn't find a single /hgg2d/ thread 😔", color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['thread'])
      @commands.cooldown(1, 3, commands.BucketType.default)
      async def gbfg(self, ctx):
          """Post the latest /gbfg/ threads"""
          threads = await self.get4chan('vg', '/gbfg/')
          if len(threads) > 0:
              msg = ""
              for t in threads:
                  msg += ':poop: https://boards.4channel.org/vg/thread/{} ▫️ *{} replies*\n'.format(t[0], t[1])
              await ctx.send(embed=self.bot.buildEmbed(title="/gbfg/ latest thread(s)", description=msg, footer="Have fun, fellow 4channeler", color=self.color))
          else:
              await ctx.send(embed=self.bot.buildEmbed(title="/gbfg/ Error", description="I couldn't find a single /gbfg/ thread 😔", color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, name='4chan')
      @commands.cooldown(1, 3, commands.BucketType.default)
      async def _4chan(self, ctx, board : str, *, term : str):
          """Search 4chan threads"""
          nsfw = ['b', 'r9k', 'pol', 'bant', 'soc', 's4s', 's', 'hc', 'hm', 'h', 'e', 'u', 'd', 'y', 't', 'hr', 'gif', 'aco', 'r']
          board = board.lower()
          if board in nsfw and not ctx.channel.is_nsfw():
              await ctx.send(embed=self.bot.buildEmbed(title=":underage: The board `{}` is restricted to NSFW channels".format(board)))
              return
          threads = await self.get4chan(board, term)
          if len(threads) > 0:
              msg = ""
              for t in threads:
                  msg += ':four_leaf_clover: https://boards.4channel.org/{}/thread/{} ▫️ *{} replies*\n'.format(board, t[0], t[1])
              await ctx.send(embed=self.bot.buildEmbed(title="4chan Search result", description=msg, footer="Have fun, fellow 4channeler", color=self.color))
          else:
              await ctx.send(embed=self.bot.buildEmbed(title="4chan Search result", description="No matching threads found", color=self.color))
  
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['reminder'])
      @commands.cooldown(1, 3, commands.BucketType.user)
      async def remind(self, ctx, duration : str, *, msg : str):
          """Remind you of something at the specified time (±30 seconds precision)
          <duration> format: XdXhXmXs for day, hour, minute, second, each are optionals"""
          id = str(ctx.author.id)
          if id not in self.bot.reminders:
              self.bot.reminders[id] = []
          if len(self.bot.reminders[id]) >= 5 and ctx.author.id != self.bot.ids.get('owner', -1):
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="Sorry, I'm limited to 5 reminders per user 🙇", color=self.color))
              return
          try:
              d = self.bot.makeTimedelta(duration)
              if d is None: raise Exception()
          except:
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="Invalid duration string `{}`, format is `NdNhNm`".format(duration), color=self.color))
              return
          if msg == "":
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="Tell me what I'm supposed to remind you 🤔", color=self.color))
              return
          if len(msg) > 200:
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="Reminders are limited to 200 characters", color=self.color))
              return
          try:
              self.bot.reminders[id].append([datetime.utcnow().replace(microsecond=0) + timedelta(seconds=32400) + d, msg]) # keep JST
              self.bot.savePending = True
              await ctx.message.add_reaction('✅') # white check mark
          except:
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", footer="I have no clues about what went wrong", color=self.color))
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['rl', 'reminderlist'])
      @commands.cooldown(1, 6, commands.BucketType.user)
      async def remindlist(self, ctx):
          """Post your current list of reminders"""
          id = str(ctx.author.id)
          if id not in self.bot.reminders or len(self.bot.reminders[id]) == 0:
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="You don't have any reminders", color=self.color))
          else:
              embed = discord.Embed(title="{}'s Reminder List".format(ctx.author.display_name), color=random.randint(0, 16777216)) # random color
              embed.set_thumbnail(url=ctx.author.avatar_url)
              for i in range(0, len(self.bot.reminders[id])):
                  embed.add_field(name="#{} ▫️ {:%Y/%m/%d %H:%M} JST".format(i, self.bot.reminders[id][i][0]), value=self.bot.reminders[id][i][1], inline=False)
              await ctx.send(embed=embed)
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['rd', 'reminderdel'])
      @commands.cooldown(2, 3, commands.BucketType.user)
      async def reminddel(self, ctx, rid : int):
          """Delete one of your reminders"""
          id = str(ctx.author.id)
          if id not in self.bot.reminders or len(self.bot.reminders[id]) == 0:
              await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="You don't have any reminders", color=self.color))
          else:
              if rid < 0 or rid >= len(self.bot.reminders[id]):
                  await ctx.send(embed=self.bot.buildEmbed(title="Reminder Error", description="Invalid id `{}`".format(rid), color=self.color))
              else:
                  self.bot.reminders[id].pop(rid)
                  if len(self.bot.reminders[id]) == 0:
                      self.bot.reminders.pop(id)
                  self.bot.savePending = True
                  await ctx.message.add_reaction('✅') # white check mark
  
      @commands.command(no_pm=True, cooldown_after_parsing=True)
      @isAuthorized()
      @commands.cooldown(1, 1, commands.BucketType.user)
      async def iam(self, ctx, *, role_name : str):
          """Add a role to you that you choose. Role must be on a list of self-assignable roles."""
          g = str(ctx.guild.id)
          roles = self.bot.assignablerole.get(g, {})
          if role_name.lower() not in roles:
              await ctx.message.add_reaction('❎') # negative check mark
          else:
              id = roles[role_name.lower()]
              r = ctx.guild.get_role(id)
              if r is None: # role doesn't exist anymore
                  await ctx.message.add_reaction('❎') # negative check mark
                  self.bot.assignablerole[g].pop(role_name.lower())
                  if len(self.bot.assignablerole[g]) == 0:
                      self.bot.assignablerole.pop(g)
                  self.bot.savePending = True
              else:
                  try:
                      await ctx.author.add_roles(r)
                  except:
                      pass
                  await ctx.message.add_reaction('✅') # white check mark
  
      @commands.command(no_pm=True, cooldown_after_parsing=True, aliases=['iamn'])
      @isAuthorized()
      @commands.cooldown(1, 1, commands.BucketType.user)
      async def iamnot(self, ctx, *, role_name : str):
          """Remove a role to you that you choose. Role must be on a list of self-assignable roles."""
          g = str(ctx.guild.id)
          roles = self.bot.assignablerole.get(g, {})
          if role_name.lower() not in roles:
              await ctx.message.add_reaction('❎') # negative check mark
          else:
              id = roles[role_name.lower()]
              r = ctx.guild.get_role(id)
              if r is None: # role doesn't exist anymore
                  await ctx.message.add_reaction('❎') # negative check mark
                  self.bot.assignablerole[g].pop(role_name.lower())
                  if len(self.bot.assignablerole[g]) == 0:
                      self.bot.assignablerole.pop(g)
                  self.bot.savePending = True
              else:
                  try:
                      await ctx.author.remove_roles(r)
                  except:
                      pass
                  await ctx.message.add_reaction('✅') # white check mark
  
      @commands.command(no_pm=True, cooldown_after_parsing=True)
      @isAuthorized()
      @commands.cooldown(1, 5, commands.BucketType.guild)
      async def lsar(self, ctx, page : int = 1):
          """List the self-assignable roles available in this server"""
          g = str(ctx.guild.id)
          if page < 1: page = 1
          roles = self.bot.assignablerole.get(str(ctx.guild.id), {})
          if len(roles) == 0:
              await ctx.send(embed=self.bot.buildEmbed(title="Error", description="No self assignable roles available on this server", color=self.color))
              return
          if (page -1) >= len(roles) // 20:
              page = ((len(roles) - 1) // 20) + 1
          fields = []
          count = 0
          for k in list(roles.keys()):
              if count < (page - 1) * 20:
                  count += 1
                  continue
              if count >= page * 20:
                  break
              if count % 10 == 0:
                  fields.append({'name':'{} '.format(self.bot.getEmote(str(len(fields)+1))), 'value':'', 'inline':True})
              r = ctx.guild.get_role(roles[k])
              if r is not None:
                  fields[-1]['value'] += '{}\n'.format(k)
              else:
                  self.bot.assignablerole[str(ctx.guild.id)].pop(k)
                  self.bot.savePending = True
              count += 1
  
          await ctx.send(embed=self.bot.buildEmbed(title="Self Assignable Roles", fields=fields, footer="Page {}/{}".format(page, 1+len(roles)//20), color=self.color))