class Settings:
	def __init__(self):
		self.path = "settings.json"
		self.defaults = OrderedDict([  ("token", ""), ("error_logging", False), ("debug", False) ])
		if not os.path.exists(self.path):
			self.json_data = self.defaults
			self.save_settings()
		else:
			current = read_json(self.path)
			if current.keys() != self.defaults.keys():
				for key in self.defaults.keys():
					if key not in current.keys():
						current[key] = self.defaults[key]
						logger.info("Adding " + str(key) + " field to settings.json")
				write_json(self.path, current)
			self.json_data = read_json(self.path)
		if self.token == "":
			raise Exception("You need to put a bot 'token' in the settings.json file. See README.md for more information")

	def save_settings(self):
		write_json(self.path, self.json_data)

	# the token for your bot. see the README.md for more info on how to get one of these
	@property
	def token(self):
		return self.json_data["token"]

	# enables/disables logging of errors to discord text chat
	@property
	def error_logging(self):
		return self.json_data.get("error_logging", False)

	# enables/disables certain features like caching
	@property
	def debug(self):
		return self.json_data.get("debug", False)

	# an api key/secret for the reddit api (if not given, the ?reddit command wont work. see that command to see the format that the api key should be in)
	@property
	def reddit(self):
		return self.json_data.get("reddit", None)

	# an optional api key for the opendota api (if not given, you will be using the free tier of the opendota api which is good for up to 50,000 calls a month)
	@property
	def odota(self):
		return self.json_data.get("odota", None)

	# an optional api key for the stratz api (if not given, you won't be able to use the stratz api)
	@property
	def stratz(self):
		return self.json_data.get("stratz", None)

	# an optional api key for the topgg api. If not given, then the bot wont update the topgg api
	@property
	def topgg(self):
		return self.json_data.get("topgg", None)

	# an optional number of shards to use for the bot. by default will use as many as it thinks it needs
	@property
	def shard_count(self):
		return self.json_data.get("shard_count", None)

	# an optional path to a place to save server and member count information as json
	@property
	def infodump_path(self):
		return self.json_data.get("infodump_path", None)

	# an optional list of test-guilds for testing slash commands
	@property
	def test_guilds(self):
		return self.json_data.get("test_guilds", None)

	@property
	def resourcedir(self):
		return "resource/" # self.json_data["resourcedir"]

	def resource(self, dir):
		return os.path.join(self.resourcedir, dir) def setup_logger():
	logger = logging.getLogger("mangologger")

	if settings.debug:
		logger.setLevel(logging.DEBUG)
	else:
		logger.setLevel(logging.INFO)

	# Console Logging
	if settings.debug:
		consoleout = logging.StreamHandler()
		logger.addHandler(consoleout)

	# JSON file logging
	timestamp = datetime.datetime.now().strftime("%Y-%m-%d__%I_%M%p")
	filehandler = logging.FileHandler(filename=f"logs/mangolog_{timestamp}.log", encoding="utf-8", mode="w")
	
	jsonformatter = jsonlogger.JsonFormatter()
	filehandler.setFormatter(jsonformatter)

	logger.addHandler(filehandler)


	return logger class BotData:
	def __init__(self):
		self.path = "botdata.json"
		self.defaults = OrderedDict([
			("userinfo" , []),
			("guildinfo" , []),
			("dotapatch", None),
			("dotablog",None)
		])
		if not os.path.exists(self.path):
			self.json_data = self.defaults
			self.save_data()
		else:
			current = read_json(self.path)
			if current.keys() != self.defaults.keys():
				for key in self.defaults.keys():
					if key not in current.keys():
						current[key] = self.defaults[key]
						logger.info("Adding " + str(key) + " field to botdata.json")
				write_json(self.path, current)
			self.json_data = read_json(self.path)

	def __getitem__(self, key):
		if key not in self.defaults:
			return self.__dict__[key]
		return self.json_data.get(key, self.defaults.get(key))

	def __setitem__(self, key, val):
		if key not in self.defaults:
			self.__dict__[key] = val
		self.json_data[key] = val
		self.save_data()

	def save_data(self):
		write_json(self.path, self.json_data)

	def userinfo(self, userid):
		if isinstance(userid, disnake.User) or  isinstance(userid, disnake.Member):
			userid = userid.id
		return UserInfo(self, userid)

	def guildinfo(self, guildid):
		if isinstance(guildid, disnake.ext.commands.Context):
			guildid = guildid.message.guild
		if isinstance(guildid, disnake.abc.GuildChannel):
			guildid = guildid.guild
		if isinstance(guildid, disnake.Guild):
			guildid = guildid.id
		if guildid is None:
			return None
		return GuildInfo(self, guildid)

	def guildinfo_list(self):
		guildinfos = []
		for data in self.json_data['guildinfo']:
			guildinfos.append(GuildInfo(self, data['id']))
		return guildinfos

	def userinfo_list(self):
		userinfos = []
		for data in self.json_data['userinfo']:
			userinfos.append(UserInfo(self, data['discord']))
		return userinfos

	def count_users_with_key(self, key):
		count = 0
		for data in self.json_data['userinfo']:
			if key in data and data[key]:
				count += 1
		return count

	# gets the command prefix
	def command_prefix(self, ctx):
		return self.command_prefix_guild(ctx) # will act the same for self.guildinfo

	def command_prefix_botmessage(self, bot, message):
		return self.command_prefix_guild(message.guild)

	def command_prefix_guild(self, guild):
		guildinfo = self.guildinfo(guild)
		if guildinfo is not None:
			return guildinfo.prefix
		else:
			return "?" class LoggingDb():
	def __init__(self, loggingdb_path):
		url = f"sqlite:///{loggingdb_path}"
		self.database_url = DatabaseURL(url)
		engine = sqlalchemy.create_engine(url)
		Base.metadata.create_all(engine)
		Session = sessionmaker(bind=engine)
		self.session = Session()
		self.loop = asyncio.get_event_loop()
		self.lock = asyncio.Lock(loop=self.loop)

	async def query_multiple(self, queries):
		async with self.lock, Database(self.database_url) as database:
			queries = map(lambda q: database.fetch_all(query=q), queries)
			return await asyncio.gather(*queries)

	async def query(self, query):
		async with self.lock, Database(self.database_url) as database:
			return await database.fetch_all(query=query)

	async def insert_row(self, database, table, row):
		await database.execute(query=table.__table__.insert().values(row2dict(row)))

	async def insert_message(self, msg, cmd):
		start_time = datetime.datetime.now()
		async with self.lock, Database(self.database_url) as database:
			message = Message()
			message.id = msg.id
			message.author_id = msg.author.id
			message.author_name = msg.author.name
			if msg.guild:
				message.server_id = msg.guild.id
			message.channel_id = msg.channel.id
			message.timestamp = msg.created_at
			message.content = msg.content
			message.clean_content = msg.clean_content
			message.mentions = "|".join(map(lambda u: str(u.id), msg.mentions))
			message.command = cmd

			await self.insert_row(database, Message, message)
			total_time = (datetime.datetime.now() - start_time).total_seconds()
			print_debug(f"insert_message(): {total_time * 1000:.2f}ms")
			return message

	async def insert_command(self, ctx):
		start_time = datetime.datetime.now()
		async with self.lock, Database(self.database_url) as database:
			command = Command()
			command.message_id = ctx.message.id
			command.command = ctx.command.name
			command.invoke_time = datetime.datetime.utcnow()
			command.status = "started"

			await self.insert_row(database, Command, command)
			total_time = (datetime.datetime.now() - start_time).total_seconds()
			print_debug(f"insert_command(): {total_time * 1000:.2f}ms")
			return command

	async def command_finished(self, ctx, status, error):
		await asyncio.sleep(1)
		async with self.lock, Database(self.database_url) as database:
			start_time = datetime.datetime.now()
			if ctx.command is None:
				return
	
			cmd = self.session.query(Command).filter_by(message_id=ctx.message.id).order_by(sqlalchemy.desc(Command.id)).first()
			if not cmd:
				logger.info("couldnt find cmd to finish")
				return
	
			cmd.status = status
			cmd.finish_time = datetime.datetime.utcnow()
			self.session.commit()
	
			total_time = (datetime.datetime.now() - start_time).total_seconds()
			print_debug(f"command_finished(): {total_time * 1000:.2f}ms")

		# async with Database(self.database_url) as database:
			# if ctx.command is None:
			# 	return # no command to finish

			# values = {
			# 	"status": status,
			# 	"finish_time": datetime.datetime.utcnow()
			# }
			# if error:
			# 	values["error"] = error

			# cmdtable = Command.__table__
			# query = cmdtable.update().where(cmdtable.c.message_id==ctx.message.id).values(**values)
			# await database.execute(query=query)

	async def insert_error(self, message, the_error, trace):
		start_time = datetime.datetime.now()
		async with self.lock, Database(self.database_url) as database:
			error = Error()
			error.message_id = message.id
			error.timestamp = datetime.datetime.utcnow()
			error.command_error = type(the_error).__name__
			error.error = str(the_error)
			error.traceback = trace

			await self.insert_row(database, Error, error)
			total_time = (datetime.datetime.now() - start_time).total_seconds()
			print_debug(f"insert_error(): {total_time * 1000:.2f}ms")
			return error

	async def insert_http_request(self, url, status, cached):
		start_time = datetime.datetime.now()
		async with self.lock, Database(self.database_url) as database:
			request = HttpRequest()

			request.url = url
			request.timestamp = datetime.datetime.utcnow()
			request.status = status
			request.was_cached = False
			request.cached = cached

			await self.insert_row(database, HttpRequest, request)
			total_time = (datetime.datetime.now() - start_time).total_seconds()
			print_debug(f"insert_http_request(): {total_time * 1000:.2f}ms")
			return request

	async def update_guilds(self, guilds):
		async with self.lock:
			guild_dict = {}
			for guild in guilds:
				guild_dict[guild.id] = guild

			# update existing guilds
			new_guild_ids = list(guild_dict.keys())
			for guild_log in self.session.query(Guild):
				if guild_log.id in new_guild_ids:
					new_guild_ids.remove(guild_log.id)

				if guild_log.leave_time is not None:
					guild_log.leave_time = None
				if guild_log.name == "<Unknown>":
					guild_log.name = guild.name

			# add new guilds
			for guild_id in new_guild_ids:
				guild = guild_dict[guild_id]
				guild_log = Guild()
				guild_log.id = guild.id
				guild_log.name = guild.name
				guild_log.join_time = guild.me.joined_at
				guild_log.leave_time = None
				self.session.add(guild_log)

			async with Database(self.database_url) as database:
				current_ids = ", ".join(list(map(lambda g: str(g), guild_dict.keys())))

				# fix all existing guilds that say they've left
				await database.execute(query=f"UPDATE guilds SET leave_time = NULL WHERE id in ({current_ids}) and leave_time is not NULL")

				# run query to remove guilds that have left
				await database.execute(query=f"UPDATE guilds SET leave_time = datetime('now','localtime') WHERE id not in ({current_ids}) and leave_time is NULL")
		self.session.commit()


	# only called manually
	def update_commands_column(self, bot):
		for message in self.session.query(Message):
			match = re.search(r"^\?([^\s]+)(\s|$)", message.content, re.IGNORECASE)
			if match:
				cmd = bot.all_commands.get(match.group(1))
				if cmd:
					message.command = cmd.name
		self.session.commit()
		logger.info("done updating logged commands!")

	# only called manually
	def add_missing_guilds(self, current_guilds):
		self.update_guilds(current_guilds, self.session)
		guilds = {}
		for message in self.session.query(Message).order_by(Message.timestamp):
			if message.server_id not in guilds:
				guilds[message.server_id] = {
					"first": message.timestamp,
					"last": message.timestamp
				}
			else:
				guilds[message.server_id]["last"] = message.timestamp

		for guild_id in guilds:
			found = False
			for guild in self.session.query(Guild):
				if str(guild.id) == str(guild_id):
					found = True
					break
			if not found:
				guild_log = Guild()
				guild_log.id = guild_id
				guild_log.name = "<Unknown>"
				guild_log.join_time = guilds[guild_id]["first"]
				guild_log.leave_time = guilds[guild_id]["last"]
				self.session.add(guild_log)
		self.session.commit() class HttpGetter:
	def __init__(self):
		self.loop = asyncio.get_event_loop()
		self.session = aiohttp.ClientSession(loop=self.loop)
		self.cache = Cache(self.loop)

	async def get(self, url, return_type="json", cache=False, errors={}, headers=None):
		if cache and self.cache.get_filename(url):
			return self.cache.get(url, return_type)

		async with self.session.get(url, headers=headers, timeout=60) as r:
			await loggingdb.insert_http_request(url, r.status, cache)
			if r.status == 200:
				if cache:
					await self.cache.save(url, return_type, r)
					if return_type == "filename":
						return self.cache.get_filename(url)

				if return_type == "json":
					return json.loads(await r.text(), object_pairs_hook=OrderedDict)
				elif return_type == "text":
					return await r.text()
				elif return_type == "bytes":
					return BytesIO(await r.read())
				else:
					raise ValueError(f"Invalid return type '{return_type}'")
			else:
				raise_error(url, r.status, errors)

	async def post(self, url, return_type="json", errors={}, body={}, headers={}):
		async with self.session.post(url, json=body, headers=headers) as r:
			await loggingdb.insert_http_request(url, r.status, False)
			if r.status == 200:
				if return_type == "json":
					return json.loads(await r.text(), object_pairs_hook=OrderedDict)
				elif return_type == "text":
					return await r.text()
				elif return_type == "bytes":
					return BytesIO(await r.read())
				else:
					raise ValueError(f"Invalid return type '{return_type}'")
			else:
				raise_error(url, r.status, errors) class MangoHelpCommand(DefaultHelpCommand):
	def __init__(self, **options):
		options["verify_checks"] = False
		super().__init__(**options)

	@property
	def bot(self):
		return self.context.bot

	async def send_bot_help(self, mapping):
		no_category = '\u200b{0.no_category}:'.format(self)
		def get_category(command, *, no_category=no_category):
			cog = command.cog
			return cog.qualified_name + ':' if cog is not None else no_category

		if self.show_all:
			# ?help all
			embed = self.embed_description(f"{self.bot.description}\n\n{text_help_server}\n\n{text_category_help}\n{text_command_help}", self.bot)
			embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.avatar.url, url="https://github.com/mdiller/MangoByte")

			commands = list(self.bot.commands)
			commands.extend(self.bot.slash_commands)
			filtered = await self.filter_commands(commands, sort=True, key=get_category)
			to_iterate = itertools.groupby(filtered, key=get_category)

			for category, commands in to_iterate:
				if category == "Owner:":
					continue
				commands = list(commands)
				if len(commands) > 0:
					embed.add_field(name=category, value=self.list_commands(commands, only_name=True), inline=False)
		else:
			# ?help
			embed = self.embed_description(f"{self.bot.description}\n\n{text_help_server}\n\n{text_category_help}\nTo show all commands, try `{{cmdpfx}}help all`", self.bot)
			embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.avatar.url, url="https://github.com/mdiller/MangoByte")
			for cog in self.bot.cogs:
				if cog == "Owner":
					continue
				embed.add_field(name=f"**{cog}**", value=self.cog_short_doc(self.bot.cogs[cog]), inline=False)
		await self.send_embed(embed)

	async def send_command_help(self, command):
		# ?help <command>
		embed = self.embed_description(command.help, command)
		embed.set_author(name=self.get_command_signature(command))
		if command.aliases:
			embed.add_field(name="Aliases", value=", ".join(command.aliases))
		await self.send_embed(embed)

	async def send_cog_help(self, cog : Cog):
		# ? help <cog>
		description = inspect.getdoc(cog)
		description += f"\n\n{text_command_help}"
		commands = cog.get_commands()
		commands.extend(cog.get_slash_commands())
		description += "\n\n**Commands:**\n" + self.list_commands(await self.filter_commands(commands))
		embed = self.embed_description(description, cog)
		embed.set_author(name=cog.__class__.__name__)
		# embed.add_field(name="Commands", value=self.list_commands(await self.filter_commands(cog..())))
		await self.send_embed(embed)

	async def send_embed(self, embed):
		dest = self.get_destination()
		await dest.send(embed=embed)
	
	# overridden to support slash commands
	async def filter_commands(self, commands, *, sort=False, key=None):
		msg_commands = list(filter(lambda c: isinstance(c, Command), commands))
		slash_commands = list(filter(lambda c: isinstance(c, InvokableSlashCommand), commands))
		
		msg_commands = await super().filter_commands(msg_commands)

		msg_commands.extend(slash_commands)
		if sort:
			if key:
				msg_commands.sort(key=key)
			else:
				msg_commands.sort(key=lambda c: c.name)
		return msg_commands

	# Overridden to ignore case for input, and to add the 'all' option
	async def command_callback(self, ctx, *, command=None):
		if command:
			command = command.lower()
			trimming_pattern = f"(^<|>$|^{re.escape(botdata.command_prefix(ctx))})"
			while re.match(trimming_pattern, command):
				command = re.sub(trimming_pattern, "", command)
			if command == "all":
				command = None
				self.show_all = True
			else:
				name = command
				if name in map(lambda c: c.lower(), ctx.bot.cogs):
					for cog in ctx.bot.cogs:
						if cog.lower() == name:
							command = cog
							break
		else:
			self.show_all = False

		await super().command_callback(ctx, command=command)

	def list_commands(self, commands, only_name=False):
		results = []
		commands = sorted(commands, key=lambda c: c.name) 
		for command in commands:
			if isinstance(command, Command) and command.name in command.aliases:
				# skip aliases
				continue
			newline = ""
			if only_name:
				newline = "`{{cmdpfx}}{0:{1}<30}`".format(command.name, u"\u00A0")
			else:
				description = command.short_doc if isinstance(command, Command) else command.description
				entry = '`{{cmdpfx}}{0:{2}<{width}} | {1}`'.format(command.name, description, u"\u00A0", width=self.get_max_size(commands))
				newline = self.shorten_text(entry)
			if isinstance(command, InvokableSlashCommand):
				newline = newline.replace("{cmdpfx}", "/")
			results.append(newline)
		if results:
			return self.fill_template("\n".join(results))
		else:
			return "`<empty>`"
	
	def get_command_signature(self, command):
		return '%s%s %s' % (botdata.command_prefix(self.context), command.qualified_name, command.signature)

	def fill_template(self, text):
		text = re.sub("\{config_help\}", get_config_help(GuildInfo.variables, "config"), text)
		text = re.sub("\{userconfig_help\}", get_config_help(UserInfo.variables, "userconfig"), text)
		text = re.sub("\{cmdpfx\}", botdata.command_prefix(self.context), text)
		text = re.sub("\n`", u"\n\u200b`", text)
		return text

	def cog_short_doc(self, cog):
		return self.fill_template(inspect.getdoc(cog).split('\n')[0])

	def embed_description(self, description, helptarget):
		if not description:
			return disnake.Embed()
		description = self.fill_template(description)
		guildinfo = botdata.guildinfo(self.context)
		if helptarget and guildinfo and guildinfo.is_disabled(helptarget):
			emoji = simple_get_emoji("command_disabled", self.context.bot)
			thing = "command" if isinstance(helptarget, Command) else "category"
			description = f"{emoji} *This {thing} has been disabled on this server*\n{description}"
		return disnake.Embed(description=description, color=disnake.Color.blue()) class General(MangoCog):
	"""Commands that don't really fit into the other categories

	Random and/or fun commands with a variety of uses"""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)
		self.donation_link = "https://www.paypal.me/dillerm"
		self.reactions = read_json(settings.resource("json/reactions.json"))
		self.questions = read_json(settings.resource("json/questions.json"))
		self.subscripts = read_json(settings.resource("json/subscripts.json"))
		self.superscripts = read_json(settings.resource("json/superscripts.json"))
		self.showerthoughts_data = read_json(settings.resource("json/showerthoughts.json"))
		self.docs_data = load_md_as_dict(settings.resource("docs.md"))
		self.words = load_words()

	@commands.command()
	async def userconfig(self, ctx, name, *, value = None):
		"""Configures the bot's user-specific settings

		Below are the different user-specific settings that you can tweak to customize mangobyte. You can get more information about a setting by typing `{cmdpfx}userconfig <settingname>`, and you can configure a setting by typing `{cmdpfx}userconfig <settingname> <value>`

		{userconfig_help}
		"""
		var = next((v for v in UserInfo.variables if v["key"] == name), None)
		if not var:
			vars_list = "\n".join(map(lambda v: f"`{v['key']}`", UserInfo.variables))
			await ctx.send(f"There is no userconfig setting called '{name}'. Try one of these:\n{vars_list}")
			return

		currentvalue = botdata.userinfo(ctx.message.author)[var["key"]]
		if not value: # We are just getting a value
			await ctx.send(embed=await botdatatypes.localize_embed(ctx, var, currentvalue, f"{self.cmdpfx(ctx)}userconfig"))
		else: # We are setting a value
			value = await botdatatypes.parse(ctx, var, value, currentvalue)
			botdata.userinfo(ctx.message.author)[var["key"]] = value
			await ctx.message.add_reaction("✅")

	@commands.slash_command()
	async def ping(self, inter : disnake.CommandInteraction, count : commands.Range[1, 20] = 1):
		"""Pongs a number of times(within reason)"""
		ping_string = ""
		for i in range(0, count):
			ping_string += "pong "
		await inter.send(ping_string)

	@commands.command()
	async def echo(self, ctx, *, message : str):
		"""Echo...

		I would hurl words into this darkness and wait for an echo, and if an echo sounded, no matter how faintly, I would send other words to tell, to march, to fight, to create a sense of the hunger for life that gnaws in us all"""
		await ctx.send(message)

	@commands.command()
	async def changelog(self, ctx):
		"""Gets a rough changelog for mangobyte

		Note that this is a very rough changelog built from git commit messages and so will sometimes not relate directly to your perspective.

		For more commit versions or better detailed information, check out the source on [GitHub](https://github.com/mdiller/MangoByte/commits/master)
		"""
		commit_url = "https://github.com/mdiller/MangoByte"
		description = f"For more information check out the [commit history]({commit_url}/commits/master) on GitHub\n"
		lines = get_changelog().split("\n")

		recent_date = 0

		for line in lines:
			if line == "":
				continue
			commit = line.split(",")
			full_sha = commit[0]
			timestamp = int(commit[1])
			small_sha = commit[2]
			message = ",".join(commit[3:])
			if timestamp > recent_date:
				recent_date = timestamp
			description += f"\n[`{small_sha}`]({commit_url}/commit/{full_sha}) {message}"

		embed = disnake.Embed(description=description, color=disnake.Color.green())

		if recent_date != 0:
			embed.timestamp = datetime.datetime.fromtimestamp(recent_date, tz=datetime.timezone.utc)
			embed.set_footer(text="Most recent change at")

		embed.set_author(name="Changelog", url=f"{commit_url}/commits/master")
		await ctx.send(embed=embed)

	@commands.command()
	async def info(self, ctx):
		"""Prints info about mangobyte"""
		github = "https://github.com/mdiller/MangoByte"
		python_version = "[Python {}.{}.{}]({})".format(*os.sys.version_info[:3], "https://www.python.org/")
		library_url = "https://github.com/DisnakeDev/disnake"

		embed = disnake.Embed(description="The juiciest unsigned 8 bit integer you eva gonna see", color=disnake.Color.green())

		embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.avatar.url, url=github)

		embed.add_field(name="Development Info", value=(
			"Developed as an open source project, hosted on [GitHub]({}). "
			"Implemented using {} and a python discord api wrapper [disnake]({})".format(github, python_version, library_url)))

		help_guild_link = "https://discord.gg/d6WWHxx"

		embed.add_field(name="Help", value=(
			f"If you want to invite mangobyte to your server/guild, click this [invite link]({invite_link}). "
			f"If you have a question, suggestion, or just want to try out mah features, check out the [Help Server/Guild]({help_guild_link})."))

		cmdpfx = self.cmdpfx(ctx)
		embed.add_field(name="Features", value=(
			f"• Answers questions (`{cmdpfx}ask`)\n"
			f"• Plays audio clips (`{cmdpfx}play`, `{cmdpfx}dota`)\n"
			f"• Greets users joining a voice channel\n"
			f"• For a list of command categories, try `{cmdpfx}help`"), inline=False)

		donate_stuff = "\n".join(map(lambda key: f"• [{key}]({donate_links[key]})", donate_links))
		embed.add_field(name="Donating", value=(
			f"If you want to donate money to support MangoByte's server costs, click one of the links below. If you want to learn more about how much I spend on MangoByte per month try `{cmdpfx}donate`.\n{donate_stuff}"))

		owner = (await self.bot.application_info()).owner

		embed.set_footer(text="MangoByte developed by {}".format(owner.name), icon_url=owner.avatar.url)

		await ctx.send(embed=embed)

	@commands.command()
	async def invite(self, ctx):
		"""Prints the invite link"""
		await ctx.send(invite_link)

	@commands.command()
	async def botstats(self, ctx):
		"""Displays some bot statistics"""
		await ctx.channel.trigger_typing()

		embed = disnake.Embed(color=disnake.Color.green())

		embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.avatar.url)

		embed.add_field(name="Servers/Guilds", value="{:,}".format(len(self.bot.guilds)))
		embed.add_field(name="Registered Users", value="{:,}".format(botdata.count_users_with_key("steam")))

		thisweek = "timestamp between datetime('now', '-7 days') AND datetime('now', 'localtime')"
		query_results = await loggingdb.query_multiple([
			f"select count(*) from messages where command is not null",
			f"select count(*) from messages where command is not null and {thisweek}",
			f"select command from messages where command is not null group by command order by count(command) desc limit 3",
			f"select command from messages where command is not null and {thisweek} group by command order by count(command) desc limit 3"
		])


		# embed.add_field(name="Commands", value=f"{query_results[0][0][0]:,}")
		embed.add_field(name="Commands (This Week)", value=f"{query_results[1][0][0]:,}")

		cmdpfx = self.cmdpfx(ctx)
		top_commands = query_results[2]
		# if len(top_commands) >= 3:
		# 	embed.add_field(name="Top Commands", value=(
		# 		f"`{cmdpfx}{top_commands[0][0]}`\n"
		# 		f"`{cmdpfx}{top_commands[1][0]}`\n"
		# 		f"`{cmdpfx}{top_commands[2][0]}`\n"))

		top_commands_weekly = query_results[3]
		if len(top_commands_weekly) >= 3:
			embed.add_field(name="Top Commands (This Week)", value=(
				f"`{cmdpfx}{top_commands_weekly[0][0]}`\n"
				f"`{cmdpfx}{top_commands_weekly[1][0]}`\n"
				f"`{cmdpfx}{top_commands_weekly[2][0]}`\n"))

		await ctx.send(embed=embed)

	@commands.command()
	async def lasagna(self, ctx):
		"""A baked Italian dish

		Contains wide strips of pasta cooked and layered with meat or vegetables, cheese, and tomato sauce."""
		lasagna_images = [
			"images/lasagna1.jpg",
			"images/lasagna2.jpg",
			"images/lasagna3.jpg",
			"images/lasagna4.jpg",
			"images/lasagna5.jpg",
			"images/lasagna6.jpg",
			"images/lasagna7.jpg",
			"images/lasagna8.jpg",
			"images/lasagna9.jpg",
		]
		await ctx.send(file=disnake.File(settings.resource(random.choice(lasagna_images))))

	@commands.command()
	async def scramble(self, ctx, *, message : str):
		"""Scrambles the insides of words"""

		def scramble_word(word):
			if len(word) < 4:
				letters = list(word)
				random.shuffle(letters)
				return "".join(letters)
			else:
				letters = list(word[1:-1])
				random.shuffle(letters)
				return word[0] + "".join(letters) + word[-1]

		results = []
		for word in message.split(" "):
			results.append(scramble_word(word))

		await ctx.send(" ".join(results))

	@commands.command(aliases=["define", "lookup", "wikipedia", "whatis"])
	async def wiki(self, ctx, *, thing : str):
		"""Looks up a thing on wikipedia
		
		Uses my own implementation of the [Wikipedia API](https://www.mediawiki.org/wiki/API:Tutorial)

		You can also try `{cmdpfx}wiki random` to get a random wiki page
		
		Note that I've had to remove the images from these (unless you're in a NSFW channel) because I have no way of checking if it is an NSFW image, and posting an NSFW image in non-NSFW channels would be against discord's ToS. Sorry about that!

		**Example:**
		`{cmdpfx}wiki potato`
		"""
		await ctx.channel.trigger_typing()

		page = await wikipedia.get_wikipedia_page(thing)

		embed = disnake.Embed(description=page.markdown)
		embed.title = f"**{page.title}**"
		embed.url = page.url

		footer_text = "Retrieved from Wikipedia"

		if page.image:
			if (not isinstance(ctx.channel, disnake.DMChannel)) and ctx.channel.is_nsfw():
				embed.set_image(url=page.image)
			else:
				footer_text += ". (Image omitted because I can't check if it is NSFW) D:"

		embed.set_footer(text=footer_text, icon_url="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Wikipedia's_W.svg/2000px-Wikipedia's_W.svg.png")
		
		await ctx.send(embed=embed)

	@commands.command()
	async def reddit(self, ctx, url_or_id):
		"""Displays a formatted reddit post

		Note that this will only get nsfw posts if you call this in an nsfw channel"""
		if settings.reddit is None:
			raise UserError("This MangoByte has not been configured to get reddit submissions. Gotta add your info to `settings.json`")

		await ctx.channel.trigger_typing()

		reddit = praw.Reddit(client_id=settings.reddit["client_id"],
			client_secret=settings.reddit["client_secret"],
			user_agent=settings.reddit["user_agent"])

		try:
			if re.search(r"(redd\.it|reddit.com)", url_or_id):
				if not re.search(r"https?://", url_or_id):
					url_or_id = "http://" + url_or_id
				submission = reddit.submission(url=url_or_id)
			else:
				submission = reddit.submission(id=url_or_id)
			description = submission.selftext
		except:
			raise UserError("Couldn't properly find that reddit submission")

		
		if submission.over_18:
			if (isinstance(ctx.channel, disnake.DMChannel)) or (not ctx.channel.is_nsfw()):
				raise UserError("That is an NSFW post, so I can't link it in this non-nsfw channel.")


		character_limit = 600
		# convert between markdown types
		description = re.sub(r"\n(?:\*|-) (.*)", r"\n• \1", description)
		description = re.sub(r"(?:^|\n)#+([^#\n]+)\n", r"\n__**\1**__ \n", description)
		description = re.sub(r"\n+---\n+", r"\n\n", description)
		description = re.sub(r"&nbsp;", r" ", description)

		description = html.unescape(description)

		if len(description) > character_limit:
			description = f"{description[0:character_limit]}...\n[Read More]({submission.shortlink})"

		embed = disnake.Embed(description=description, color=disnake.Color(int("ff4500", 16)))
		embed.set_footer(text=f"/r/{submission.subreddit}", icon_url="https://images-na.ssl-images-amazon.com/images/I/418PuxYS63L.png")

		embed.title = submission.title
		embed.url = submission.shortlink

		url_ext = submission.url.split(".")[-1]

		if url_ext in ["gifv", "gif", "png", "jpg", "jpeg"]:
			image_url = submission.url
			if url_ext == "gifv":
				image_url = image_url.replace(".gifv", ".gif")
			embed.set_image(url=image_url)

		await ctx.send(embed=embed)

	@commands.command(aliases=["quote", "showerthoughts", "thought" ])
	async def showerthought(self, ctx):
		"""Gets a top post from r/ShowerThoughts
		
		Gets a random post from the [r/ShowerThoughts](https://www.reddit.com/r/Showerthoughts/top/?sort=top&t=all) subreddit. Looks through the list of the all time top posts for the subreddit
		"""
		await ctx.channel.trigger_typing()

		thought = random.choice(self.showerthoughts_data)

		author = thought["author"]
		author = f"u/{author}" if author else "[deleted]"

		embed = disnake.Embed()

		embed.description = thought["title"]
		embed.timestamp = datetime.datetime.fromtimestamp(thought["timestamp"], tz=datetime.timezone.utc)
		embed.set_footer(text=author)

		await ctx.send(embed=embed)

	@commands.command(hidden=True, aliases=["restapi"])
	async def restget(self, ctx, url):
		"""Gets a json response from a rest api and returns it"""
		await ctx.channel.trigger_typing()
		data = await httpgetter.get(url)

		filename = settings.resource("temp/response.json")
		write_json(filename, data)
		await ctx.send(file=disnake.File(filename))
		os.remove(filename)

	@commands.command()
	async def ask(self, ctx, *, question : str=""):
		"""Answers any question you might have"""
		random.seed(question)
		for check in self.questions:
			if re.search(check["regex"], question):
				clip = await self.get_clip(random.choice(check['responses']), ctx)
				await ctx.send(clip.text)
				try:
					await self.play_clip(clip, ctx)
				except AudioPlayerNotFoundError:
					pass # Not needed for this 
				return
		logger.info("didnt match anything for ask")

	@commands.command()
	async def insult(self, ctx):
		"""Gets a nice insult for ya

		Mention someone in discord and I'll insult them instead of you

		**Example:**
		`{cmdpfx}insult`
		`{cmdpfx}insult @InnocentMan`
		"""
		start = "You "
		start_local = start

		template = "{animal|food|furniture|instrument:NOSPACE}-{body_part_ed} {relation} of a {animal|furniture}"

		if ctx.message.mentions:
			user = ctx.message.mentions[0]
			if user.id == ctx.guild.me.id:
				template = "lovely fellow"
			start = f"{user.mention}, you're a "
			start_local = f"{user.name}, you're a "

		result = fill_word_template(template, self.words)

		await ctx.send(start + result)
		if ctx.guild and ctx.guild.me.voice:
			await self.play_clip(f"tts:{start_local}{result}", ctx)
		
	
	@commands.command(aliases=["random"])	
	async def random_number(self, ctx, maximum : int, minimum : int = 0):
		"""Gets a random number between the minimum and maximum

		The min and max integer bounds are **inclusive**

		The command will be able to figure out which number is the minimum and which is the maximum if they are put in backwards. If one number is entered, it is assumed to be the maximum, and the default minimum is 0

		**Example:**
		`{cmdpfx}random 5`
		`{cmdpfx}random 1 10`
		"""
		result = None
		if maximum < minimum:
			result = random.randint(maximum, minimum)
		else:
			result = random.randint(minimum, maximum)
		await ctx.send(result)
	
	@commands.command(aliases=["pickone"])
	async def choose(self, ctx, *options):
		"""Randomly chooses one of the given options

		You must provide at least one option for the bot to choose, and the options should be separated by spaces
		
		**Example:**
		`{cmdpfx}choose Dota2 Fortnite RocketLeague`
		`{cmdpfx}choose green red blue`
		"""
		if not len(options) > 0:
			raise UserError("You gotta give me a couple different options, separated by spaces")
		await ctx.send(random.choice(options))

	@commands.command(aliases=["documentation"])
	async def docs(self, ctx, *, topic : str = None):
		"""Shows the documentation for the given topic

		If no parameters are given, then this shows the available documentation
		
		**Example:**
		`{cmdpfx}docs`
		`{cmdpfx}docs Match Filter`
		`{cmdpfx}docs matchfilter`
		"""
		if topic is None:
			embed = disnake.Embed()
			embed.title = "Available Topics"
			embed.description = "\n".join(map(lambda name: f"• {name}", list(self.docs_data.keys())))
			await ctx.send(embed=embed)
			return
		clean_topic = topic.lower().replace(" ", "")
		found_topic = None
		for name in self.docs_data:
			simple_name = name.lower().replace(" ", "")
			if clean_topic in simple_name:
				found_topic = name
				break
		if found_topic is None:
			raise UserError(f"Couldn't find a topic called '{topic}'")

		embed = disnake.Embed()
		embed.title = found_topic
		embed.description = self.docs_data[found_topic]
		await ctx.send(embed=embed)

	@tasks.loop(hours=12)
	async def update_topgg(self):
		logger.info("update_topgg() entered")
		if settings.debug or (settings.topgg is None):
			return # nothing to do here

		bot_id = self.bot.user.id
		topgg_token = settings.topgg
		guilds_count = len(self.bot.guilds)

		try:
			url = f"https://top.gg/api/bots/{bot_id}/stats"
			body = {
				"server_count": guilds_count
			}
			headers = {
				"Authorization": topgg_token
			}
			response = await httpgetter.post(url, body=body, headers=headers)
		except HttpError as e:
			await self.send_owner(f"Updating top.gg failed with {e.code} error")

	@tasks.loop(hours=12)
	async def do_infodump(self):
		logger.info("do_infodump() entered")
		if not settings.infodump_path:
			return # nothing to do here

		guilds_count = len(self.bot.guilds)
		member_count = botdata.count_users_with_key("steam")

		data = {
			"servers": guilds_count,
			"registered_users": member_count
		}
		try:
			with open(settings.infodump_path, "w+") as f:
				f.write(json.dumps(data))
		except Exception as e:
			await self.send_owner(f"do_infodump failed w/ exception: {e}")

	@tasks.loop(minutes=5)
	async def check_dota_patch(self):
		logger.info("check_dota_patch() entered")
		url = "https://www.dota2.com/patches/"
		try:
			text = await httpgetter.get(url, return_type="text")
		except HttpError as e:
			logger.info(f"patches update failed with http {e.code} error")
			await self.send_owner(f"patches update failed the check with a http {e.code} error")
			return # failed, so return
		except Exception as e:
			etype = str(type(e).__name__)
			logger.info(f"patches update failed the check w/ exception {etype}: {e}")
			await self.send_owner(f"patches update failed the check w/ exception {etype}: {e}")
			return # failed, so return
		soup = BeautifulSoup(text, "html.parser")

		logger.info("patch parse starting")

		current_patch = soup.find(name="title").contents[0]
		old_patch = botdata["dotapatch"]

		if old_patch == current_patch:
			return # thats the current patch, do nothing
		if current_patch.strip() == "Gameplay Update":
			return # thats what happens when theyre tryna switch it and theyre in the process, so give it a minute and try again later
		logger.info(f"\"{current_patch}\"")
		logger.info(current_patch == "Gameplay Update")
		logger.info(str(current_patch) == "Gameplay Update")
		await self.send_owner(f"patches update triggered: (new one is '{current_patch}', old one was '{old_patch}')")
		botdata["dotapatch"] = current_patch

		def count_class_in_id(element_id, classname):
			element = soup.find(id=element_id)
			if element is None:
				return 0
			return len(element.find_all(lambda tag: tag.get("class") == [ classname ]))

		description = ""
		section_counts = OrderedDict()
		section_counts["General"] = count_class_in_id("GeneralSection", "PatchNote")
		section_counts["Item"] = count_class_in_id("ItemsSection", "ItemName")
		section_counts["Hero"] = count_class_in_id("HeroesSection", "HeroName")
		for section in section_counts:
			count = section_counts[section]
			if count > 0:
				description += f"\n{count} {section} changes"

		image_meta_tag = soup.find(name="meta", attrs={ "property" : "og:image" })

		if image_meta_tag is not None:
			description = ""

		if description == "" and image_meta_tag is None:
			description = "*Couldn't parse the changes.*"

		# we can improve this embed later but for now this is what we got
		embed = disnake.Embed(timestamp=datetime.datetime.utcnow())
		embed.title = current_patch
		embed.url = url
		embed.description = description
		embed.set_thumbnail(url="https://cdn.cloudflare.steamstatic.com/apps/dota2/images/blog/play/dota_logo.png")
		if image_meta_tag:
			embed.set_image(url=image_meta_tag["content"])

		messageables = []
		guildinfos = botdata.guildinfo_list()
		for guildinfo in guildinfos:
			if guildinfo.dotapatchchannel is not None:
				channel = self.bot.get_channel(guildinfo.dotapatchchannel)
				if channel is not None:
					messageables.append(channel)
				else:
					logger.info(f"couldn't find channel {guildinfo.dotapatchchannel} when announcing dota patches")

		userinfos = botdata.userinfo_list()
		for userinfo in userinfos:
			if userinfo.dmdotapatch:
				user = self.bot.get_user(userinfo.discord)
				if user is not None:
					messageables.append(user)
				else:
					logger.info(f"couldn't find user {userinfo.discord} when announcing dota patches")

		tasks = []
		for messageable in messageables:
			tasks.append(messageable.send(embed=embed))

		bundler = AsyncBundler(tasks)
		logger.info("waiting for dota patch bundle to complete")
		await bundler.wait()
		logger.info("dota patch bundle completed")
		await self.send_owner("__Dota Patch Sent!__\n" + bundler.status_as_string())


	@commands.Cog.listener()
	async def on_message(self, message):
		if message.author == self.bot.user:
			return # ignore stuff from myself

		if message.guild is None:
			return # only keep going if we're in a guild
		guildinfo = botdata.guildinfo(message.guild.id)

		if message.author.bot and (message.author.id in guildinfo.allowedbots) or (message.webhook_id and guildinfo.allowwebhooks):
			# execute this command from a bot because we're allowing it
			ctx = await self.bot.get_context(message)
			await self.bot.invoke(ctx)

		if message.content.startswith(self.cmdpfx(message.guild)):
			return # ignore stuff that starts with the command prefix

		if not guildinfo.reactions:
			return # only keep going for guilds with reactions enabled

		random.seed(message.content)

		for check in self.reactions:
			expression = check["regex"]
			if check.get("word"):
				expression = "\\b({})\\b".format(expression)
				match = re.search(expression, message.clean_content, re.IGNORECASE)
			else:
				match = re.search(expression, message.clean_content)
			if match and (random.random() < check.get("chance", 1.0)):
				await message.add_reaction(random.choice(check["reaction"]))
				break

	@commands.Cog.listener()
	async def on_command(self, ctx):
		msg = await loggingdb.insert_message(ctx.message, ctx.command.name)
		await loggingdb.insert_command(ctx)
		logger.info(msg)

	@commands.Cog.listener()
	async def on_command_completion(self, ctx):
		await loggingdb.command_finished(ctx, "completed", None)

	@commands.Cog.listener()
	async def on_guild_join(self, guild):
		await loggingdb.update_guilds(self.bot.guilds)

	@commands.Cog.listener()
	async def on_guild_remove(self, guild):
		await loggingdb.update_guilds(self.bot.guilds)

	@commands.command(aliases=[ "tipjar", "donation" ])
	async def donate(self, ctx):
		"""Posts the donation information"""
		embed = disnake.Embed()

		donate_stuff = "\n".join(map(lambda key: f"• [{key}]({donate_links[key]})", donate_links))
		embed.description = "I host MangoByte on [DigitalOcean](https://www.digitalocean.com), which costs `$15` per month. "
		embed.description += "Mango makes 100,000+ api calls to opendota per month, which adds up to a bit over `$10` a month. (the [api calls start costing money](https://www.opendota.com/api-keys) if you do over 50,000 a month). "
		embed.description += "I have a job, and MangoByte won't be going down anytime soon, but if you want to help with the server costs, or just support me because you feel like it, feel free to donate using any of the links below. "
		embed.description += "I don't have any paid benefits/features at the moment for people who donate, but the support is definetly appreciated! "
		embed.description += f"\n\n{donate_stuff}"

		await ctx.send(embed=embed)


	@commands.command(aliases=[ "kitten", "cats", "kittens", "minnie", "minerva" ])
	async def cat(self, ctx):
		"""Gets a picture of my cat

		These are pictures of my (the developer of mangobyte) cat. Shes a bit over a year old now. Her name is Minnie. Short for Minerva. Also known as "Kitten", "Sneakerdoodle", or "Noodle." Shes a good kitten. """
		cat_dir = settings.resource("images/cat")
		imagepath = os.path.join(cat_dir, random.choice(os.listdir(cat_dir)))
		await ctx.send(file=disnake.File(imagepath))

	@commands.command(aliases=[ "dogs", "doggos", "doggo", "comet", "fizzgig" ])
	async def dog(self, ctx):
		"""Gets a picture of one of my dogs

		These are pictures of my (the developer of mangobyte) dogs. Thier names are Fizzgig and Comet. One is floof. Other is big doggo. Floof older. Both good boys. """
		cat_dir = settings.resource("images/dog")
		imagepath = os.path.join(cat_dir, random.choice(os.listdir(cat_dir)))
		await ctx.send(file=disnake.File(imagepath)) class Audio(MangoCog):
	"""For playing audio in a voice channel

	For dota-related audio commands, try `{cmdpfx}help dotabase`"""

	def __init__(self, bot):
		MangoCog.__init__(self, bot)
		self.audioplayers = []
		self.local_clipinfo = self.init_local_clipinfo()

	def init_local_clipinfo(self):
		infofile = settings.resource("clips/clipinfo.json")
		if os.path.isfile(infofile):
			clipinfos = read_json(infofile)
		else:
			clipinfos = {}
		clipsdir = settings.resource("clips/")
		for root, dirs, files in os.walk(clipsdir):
			for file in files:
				match = re.search(f"clips[/\\\\]((?:.*[/\\\\])?([^/\\\\]+)\.(?:{audio_extensions}))", os.path.join(root, file))
				if match:
					path = match.group(1)
					name = match.group(2)
					if name not in clipinfos:
						found = False
						for clipname in clipinfos:
							if clipinfos[clipname]["path"] == path:
								found = True
								break
						if not found:
							info = { "path": path }
							in_dir = re.search(f"(.+)[/\\\\](?:.+)\.(?:{audio_extensions})", path)
							if in_dir:
								info["tags"] = in_dir.group(1)
							clipinfos[name] = info

		write_json(infofile, clipinfos)
		return clipinfos

	def save_local_clipinfo(self):
		infofile = settings.resource("clips/clipinfo.json")
		write_json(infofile, self.local_clipinfo)


	# gets the audioplayer for the current guild/channel/context
	async def audioplayer(self, ctx, error_on_none=True):
		# TODO: ACCOUNT FOR WHEN THIS MESSAGE IS A PM
		if isinstance(ctx, disnake.ext.commands.Context):
			if ctx.message.guild is None: # This is a private channel, so give it user
				ctx = ctx.message.author
			else:
				ctx = ctx.message.guild

		if isinstance(ctx, disnake.User):
			author = ctx
			for audioplayer in self.audioplayers:
				member = audioplayer.guild.get_member(author.id)
				if member and member.voice and audioplayer.voice and audioplayer.voice.channel.id == member.voice.channel.id:
					if botdata.guildinfo(audioplayer.guild).is_banned(member):
						raise AudioPlayerNotFoundError("Nice try, but you're banned in the voice channel that I'm in")
					return audioplayer
			if error_on_none:
				raise AudioPlayerNotFoundError("You're not in any voice channels that I'm in")
			else:
				return None
		elif isinstance(ctx, disnake.Guild):
			guild = ctx
		elif isinstance(ctx, disnake.abc.GuildChannel):
			guild = ctx.guild
		else:
			raise ValueError(f"Incorrect type '{type(ctx)}' given to audioplayer function")

		for audioplayer in self.audioplayers:
			if audioplayer.guild == guild:
				return audioplayer

		if error_on_none:
			raise AudioPlayerNotFoundError(f"I'm not in a voice channel on this server/guild. Have an admin do `{botdata.command_prefix(ctx)}summon` to put me in one.")
		else:
			return None

	# Connects an audioplayer for the correct guild to the indicated channel
	async def connect_voice(self, channel):
		if not isinstance(channel, disnake.abc.GuildChannel):
			channel = self.bot.get_channel(channel)
		if channel is None:
			raise UserError("channel not found")
			return

		audioplayer = await self.audioplayer(channel, error_on_none=False)
		if audioplayer is not None:
			await audioplayer.update_guild()
			await audioplayer.connect(channel)
		else:
			audioplayer = AudioPlayer(self.bot, channel.guild)
			await audioplayer.connect(channel)
			self.audioplayers.append(audioplayer)

	async def disconnect(self, guild):
		if guild.me and guild.me.voice:
			voice = next((voice for voice in self.bot.voice_clients if voice.guild == guild), None)
			if voice:
				logger.info("calling disconnect!")
				await voice.disconnect(force=True)
			else:
				logger.info("can't find voice to disconnect!\nvoices available:")
				for v in self.bot.voice_clients:
					logger.info(v.vcid)
		audioplayer = await self.audioplayer(guild, False)
		if audioplayer is not None:
			self.audioplayers.remove(audioplayer)


	@commands.command()
	async def play(self, ctx, *, clip : str):
		"""Plays an audio clip

		Example:
		`{cmdpfx}play hello`

		For a complete list of the available clips, try `{cmdpfx}playlist`

		This can also play other clip types via their ClipID
		Example:
		`{cmdpfx}play tts:hello there`"""
		if ":" not in clip:
			try:
				await self.play_clip(f"local:{clip}", ctx)
			except ClipNotFound:
				dotabase = self.bot.get_cog("Dotabase")
				if dotabase:
					chat_clip = dotabase.get_chatwheel_sound_clip(clip)
					if chat_clip:
						await self.play_clip(chat_clip, ctx)
						return
				await ctx.send(f"'{clip}' is not a valid clip. 🤦 Try {self.cmdpfx(ctx)}playlist.")
		else:
			await self.play_clip(clip, ctx)

			

	@commands.command(aliases=["playlist"])
	async def clips(self, ctx, tag : str=None):
		"""Lists the local audio clips available for the play command

		Calling this command with no arguments gets you a list of all of the clips

		To get the clips that have a specific tag, do `{cmdpfx}clips <tag>`

		To get a list of all of the possible clip tags, try `{cmdpfx}clips tags`

		You can also do `{cmdpfx}clips new` to get the 10 newest clips"""
		message = ""
		clips = []
		sort = True

		if tag is None:
			message += "\n**Clips:**\n"
			for clipname in self.local_clipinfo:
				clips.append(clipname)
		elif tag in [ "recent", "latest", "new" ]:
			clips = {}
			for clipname in self.local_clipinfo:
				clips[clipname] = os.path.getctime(settings.resource(f"clips/{self.local_clipinfo[clipname]['path']}"))
			clips = list(map(lambda x: x[0], sorted(clips.items(), key=lambda x: x[1], reverse=True)))
			clips = clips[:10]
			sort = False
		elif tag in [ "tags", "sections" ]:
			message += "\n**Tags:**\n"
			for clipname in self.local_clipinfo:
				tags = self.local_clipinfo[clipname].get("tags")
				if tags:
					tags = tags.split("|")
					for t in tags:
						if t not in clips:
							clips.append(t)
		else:
			for clipname in self.local_clipinfo:
				info = self.local_clipinfo[clipname]
				tags = info.get("tags")
				if tags:
					tags = tags.split("|")
					if tag in tags:
						clips.append(clipname)
						continue
				if len(tag) > 3 and tag.lower() in info.get("author", "").lower():
					clips.append(clipname)
			if not clips:
				raise UserError("No clips not found for that tag")

		if len(clips) > 0:
			if sort:
				clips.sort()
			clip_format = "`{}` "
			if len(clips) <= 10:
				clip_format = "`{}`\n"
			for clip in clips:
				message += clip_format.format(clip)
		await ctx.send(message)

	@commands.command()
	async def playurl(self, ctx, mp3url : str):
		"""Plays an mp3 file at a url

		Make sure to use http, not https"""
		await self.play_clip("url:" + mp3url, ctx)

	@commands.command()
	async def stop(self, ctx):
		"""Stops the currently playing audio

		Also empties the clip queue
		"""
		audioplayer = await self.audioplayer(ctx)
		while not audioplayer.clipqueue.empty():
			try:
				audioplayer.clipqueue.get()
			except queue.Empty:
				continue
		if audioplayer.voice is not None:
			audioplayer.voice.stop()

	@commands.command()
	async def replay(self, ctx):
		"""Replays the last played clip
		"""
		last_clip = (await self.audioplayer(ctx)).last_clip
		if last_clip == None:
			await ctx.send("Nobody said anythin' yet")
			return

		# If its not a temp file
		await ctx.send("Replaying " + last_clip.clipid)
		await self.play_clip(last_clip, ctx)

	@commands.command()
	async def clipinfo(self, ctx, clipid=None):
		"""Gets information and a file for the given clip

		Not giving a clipid will print info about the last clip played

		clipid is specified like this:
		`local:shitpickle`
		`dota:shredder_timb_ally_01`
		"""
		if clipid is None:
			if (await self.audioplayer(ctx)).last_clip == None:
				await ctx.send("Nobody said anythin' yet")
				return
			clipid = (await self.audioplayer(ctx)).last_clip.clipid

		try:
			clip = await self.get_clip(f"local:{clipid}", ctx)
		except ClipNotFound:
			clip = await self.get_clip(clipid, ctx)

		await ctx.channel.trigger_typing()

		if clip.type() == "url":
			filename = clip.name.split("/")[-1]
		else:
			filename = clip.name

		filename = re.sub(r"[^a-zA-Z0-9]", "", filename)

		if filename == "" or len(filename) > 32:
			filename = clip.type()

		filename += os.path.splitext(clip.audiopath)[1]

		content = f"ClipID: **{clip.clipid}**"
		clip_info_embed = await clip.get_info_embed()
		# if clip_info != "":
		# 	content += f"\n\n{clip_info}"

		await ctx.send(embed=clip_info_embed)

		try:
			await ctx.send(file=disnake.File(clip.audiopath, filename=filename))
		except FileNotFoundError as e:
			# The file is probably actually a url
			fp = urllib.request.urlopen(clip.audiopath)
			await ctx.send(file=disnake.File(fp, filename=filename))
			fp.close()

	@commands.command()
	async def tts(self, ctx, *, message : str):
		"""Like echo but for people who can't read

		Talks in whatever voice channel mangobyte is currently in
		
		Example:
		`{cmdpfx}tts Hello I'm a bot`
		"""
		await self.do_tts(ctx.message.clean_content[5:], ctx)


	async def do_tts(self, text, ctx):
		gtts_fixes = read_json(settings.resource("json/gtts_fixes.json"))
		text = text.replace("\n", " ")
		for key in gtts_fixes:
			pattern = f"\\b({key})\\b" if not key.startswith("regex:") else re.sub("^regex:", "", key)
			text = re.sub(pattern, gtts_fixes[key], text, re.IGNORECASE)
		await self.play_clip("tts:" + text, ctx)


	@commands.command()
	async def ttsclip(self, ctx, *, clip : str):
		"""Tries to text-to-speech the given clip

		Only works on clips that have text specified

		Example:
		`{cmdpfx}ttsclip yodel`
		"""
		if ":" not in clip:
			try:
				clip = await self.get_clip(f"local:{clip}", ctx)
			except ClipNotFound:
				await ctx.send(f"'{clip}' is not a valid clip. 🤦 Try `{self.cmdpfx(ctx)}playlist`")
				return
		else:
			clip = await self.get_clip(clip, ctx)
		text = clip.text.lower()
		if text == "":
			await ctx.send(f"I can't read this clip for tts 😕. Try a different one.")
			return

		await self.play_clip(f"tts:{text}", ctx)

	@commands.command(aliases= [ "stts" ])
	async def smarttts(self, ctx, *, message : str):
		"""Automatically find the best fit for the tts given

		First checks local clips (like `{cmdpfx}play`), then checks to see if it is an audio url, then checks if it's a dota chatwheel message, then checks if there is an exact match for a dota response clip, and if none of the above is found, does a simple tts clip"""
		await self.do_smarttts(message, ctx)

	async def do_smarttts(self, message, ctx):
		if message == "" or not message:
			return # dont say anything if theres nothin to be said
		simple_message = re.sub(r'[^a-z0-9\s_]', r'', message.lower())
		try:
			await self.play_clip(f"local:{simple_message}", ctx)
			return # Clip played successfully so we're done
		except ClipNotFound:
			pass
		if re.match(f'^https?://.*\.({audio_extensions})$', message):
			await self.play_clip(f"url:{message}", ctx)
			return
		dotabase = self.bot.get_cog("Dotabase")
		if dotabase:
			if simple_message in [ "haha", "lol" ]:
				response = await dotabase.get_laugh_response()
				await dotabase.play_response(response, ctx)
				return

			clip = dotabase.get_chatwheel_sound_clip(message)
			if clip:
				await self.play_clip(clip, ctx)
				return
			query = await dotabase.smart_dota_query(message.split(" "), [], exact=True)
			if query:
				await dotabase.play_response_query(query, ctx)
				return

		for clipname in self.local_clipinfo:
			clip = self.local_clipinfo[clipname]
			simple_text = re.sub(r'[^a-z0-9\s_]', r'', clip.get("text", "").lower())
			if simple_text == "":
				continue
			if simple_message == simple_text:
				await self.play_clip(f"local:{clipname}", ctx)
				return

		await self.do_tts(message, ctx)

	@commands.Cog.listener()
	async def on_message(self, message):
		if message.guild and (not message.content.startswith(self.cmdpfx(message.guild))) and message.author.id != self.bot.user.id:
			guildinfo = botdata.guildinfo(message.guild)
			if guildinfo.is_banned(message.author):
				return # banned users cant talk
			if message.author.bot:
				if message.webhook_id:
					if not guildinfo.allowwebhooks:
						return # if this is a webhook then ignore it because we're not allowing it
				else:
					if message.author.id not in guildinfo.allowedbots:
						return # ignore bots unless theyre explicitly allowed
			ttschannel = guildinfo.ttschannel
			if ttschannel == message.channel.id:
				if message.content.startswith("//") or message.content.startswith("#"):
					return # commented out stuff should be ignored
				try:
					if guildinfo.announcetts:
						name = message.author.name
						if guildinfo.usenickname and message.author.nick:
							name = message.author.nick
						name = await self.fix_name(name)
						await self.do_tts(f"{name} says", message.guild)
					if guildinfo.simpletts:
						await loggingdb.insert_message(message, "tts")
						await self.do_tts(message.clean_content, message.guild)
					else:
						await loggingdb.insert_message(message, "smarttts")
						await self.do_smarttts(message.clean_content, message.guild)
				except AudioPlayerNotFoundError as e:
					if not guildinfo.ttschannelwarn:
						return # just dont warn em if theyve said to not warn
					try:
						await message.channel.send(e.message)
					except disnake.errors.Forbidden as e:
						logger.info("on_message usererror blocked because permissions")
						pass
				except UserError as e:
					try:
						await message.channel.send(e.message)
					except disnake.errors.Forbidden as e:
						logger.info("on_message usererror blocked because permissions")
						pass
				except Exception as e:
					try:
						await message.channel.send("Uh-oh, sumthin dun gone wrong 😱")
					except disnake.errors.Forbidden as e:
						logger.info("on_message usererror blocked because permissions")
						pass
					await report_error(message, TtsChannelError(e))


	@commands.command()
	async def later(self, ctx):
		"""Tells you how much later it is

		Theres 19 different ones"""
		await self.play_clip("local:later{}".format(randint(1,19)), ctx)

	# fixes discord user names which either are in all caps or have a number serving as a letter
	async def fix_name(self, name):
		# If all upper case or numbers n stuff, make all lower case
		if re.match(r"^[^a-z]*$", name):
			name = name.lower()

		# Simple replacing of all numbers that appear to be acting as letters
		letternumbers = { "0": "o", "1": "i", "3": "e", "4": "a", "7": "t" }
		for num in letternumbers:
			name = re.sub("{}([A-Za-z])".format(num), r"{}\1".format(letternumbers[num]), name)
		return name

	#function called when this event occurs
	@commands.Cog.listener()
	async def on_voice_state_update(self, member, before, after):
		channel_id = "not sure yet"
		try:
			if member.bot and member.id != self.bot.user.id:
				return # ignore bots except for mahself
			if before and after and before.channel == after.channel:
				return # if the member didnt change channels, dont worry about it
			if before and before.channel and botdata.guildinfo(before.channel.guild).outros:
				beforeplayer = await self.audioplayer(before.channel, error_on_none=False)
				if beforeplayer is not None and beforeplayer.voice is not None and beforeplayer.voice.channel.id == before.channel.id:
					ctx = before.channel.guild
					guildinfo = botdata.guildinfo(before.channel.guild)
					userinfo = botdata.userinfo(member.id)
					channel_id = before.channel.id

					if member.id == self.bot.user.id:
						return # dont play outros for self, thatd be a bug

					try:
						outroclip = userinfo.outro
						if outroclip:
							outroclip = await self.get_clip(userinfo.outro, ctx)
							if outroclip.audiolength > botdatatypes.max_intro_outro_length + 0.5:
								userinfo.set_default(ctx, "outro")
								outroclip = userinfo.outro
					except:
						userinfo.set_default(ctx, "outro")
						outroclip = userinfo.outro

					outrotts = userinfo.outrotts
					name = member.name
					if guildinfo.usenickname and member.nick:
						name = member.nick


					text = (await self.fix_name(name)) + " " + outrotts
					logger.info(text)

					await asyncio.sleep(0.5)
					if not outroclip is None:				
						await self.play_clip(outroclip, before.channel)
					await self.play_clip("tts:" + text, before.channel)
			if after and after.channel and botdata.guildinfo(after.channel.guild).intros:
				afterplayer = await self.audioplayer(after.channel, error_on_none=False)
				if afterplayer is not None and afterplayer.voice is not None and afterplayer.voice.channel.id == after.channel.id:
					ctx = after.channel.guild
					guildinfo = botdata.guildinfo(after.channel.guild)
					channel_id = after.channel.id
					if member.id == self.bot.user.id:
						guildinfo.voicechannel = after.channel.id
						return # dont play intros for self.

					userinfo = botdata.userinfo(member.id)

					try:
						introclip = userinfo.intro
						if introclip:
							introclip = await self.get_clip(userinfo.intro, ctx)
							if introclip.audiolength > botdatatypes.max_intro_outro_length + 0.5:
								userinfo.set_default(ctx, "intro")
								introclip = userinfo.intro
					except:
						userinfo.set_default(ctx, "intro")
						introclip = userinfo.intro

					introtts = userinfo.introtts
					name = member.name
					if guildinfo.usenickname and member.nick:
						name = member.nick

					# Special case for default
					if userinfo.intro == "local:helloits" and introtts == "it's":
						introtts = ""

					text = introtts + " " + await self.fix_name(name)
					logger.info(text + " joined the channel")

					await asyncio.sleep(3)
					if not introclip is None:
						await self.play_clip(introclip, after.channel)
					await self.play_clip("tts:" + text, after.channel)
		except UserError as e:
			logger.error(f"Bad voice channel connection to ({channel_id}) from on_voice_state_update: {e.message}") class Dotabase(MangoCog):
	"""For information about Dota 2, and playing hero responses

	Interfaces with [dotabase](http://github.com/mdiller/dotabase). Check out [dotabase.dillerm.io](http://dotabase.dillerm.io) if you want to see a website that interfaces with dotabase."""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)
		self.session = session
		self.criteria_aliases = read_json(settings.resource("json/criteria_aliases.json"))
		self.hero_stat_categories = read_json(settings.resource("json/hero_stats.json"))
		self.hero_aliases = {}
		self.item_aliases = {}
		self.leveled_hero_stats = [] # by level (0 is null, and 1-30 are filled in)
		self.hero_regex = ""
		self.item_regex_1 = ""
		self.item_regex_2 = ""
		self.patches_regex = ""
		self.build_helpers()
		self.vpkurl = "http://dotabase.dillerm.io/dota-vpk"
		drawdota.init_dota_info(self.get_hero_infos(), self.get_item_infos(), self.get_ability_infos(), self.vpkurl)

	def build_helpers(self):
		def clean_input(t):
			return re.sub(r'[^a-z1-9\s]', r'', str(t).lower())
		for hero in session.query(Hero):
			aliases = hero.aliases.split("|")
			for alias in aliases:
				self.hero_aliases[alias] = hero.id
				self.hero_aliases[alias.replace(" ", "")] = hero.id

		patches_patterns = []
		for patch in session.query(Patch).filter(Patch.timestamp != None):
			patches_patterns.append(patch.number.replace(".", "\\."))
		self.patches_regex = f"(?:{'|'.join(patches_patterns)})"

		item_patterns = []
		secondary_item_patterns = []
		for item in session.query(Item).filter(~Item.localized_name.contains("Recipe")):
			aliases = item.aliases.split("|")
			aliases.append(clean_input(item.localized_name))
			for alias in aliases:
				if alias not in self.item_aliases:
					self.item_aliases[alias] = item.id
					self.item_aliases[alias.replace(" ", "")] = item.id
			pattern = re.sub(r"[^a-z' ]", "", item.localized_name.lower())
			pattern = pattern.replace("'", "'?")
			if " " in pattern:
				secondary_item_patterns.extend(pattern.split(" "))
			item_patterns.append(pattern)
		self.item_regex_1 = f"(?:{'|'.join(item_patterns)})"
		item_patterns.extend(secondary_item_patterns)
		self.item_regex_2 = f"(?:{'|'.join(item_patterns)})"

		for crit in session.query(Criterion).filter(Criterion.matchkey == "Concept"):
			self.criteria_aliases[crit.name.lower()] = crit.name

		pattern_parts = {}
		for alias in self.hero_aliases:
			parts = []
			if len(alias) > 2:
				tempstring = ""
				for i in range(2, len(alias)):
					tempstring += alias[i]
					parts.append(tempstring)
			prefix = alias[:2]
			if not prefix in pattern_parts:
				pattern_parts[prefix] = []
			pattern_parts[prefix].extend(parts)
		patterns = []
		for prefix in pattern_parts:
			parts = list(set(pattern_parts[prefix]))
			parts = sorted(parts, key=lambda p: len(p), reverse=True)
			if len(parts) > 0:
				result = f"{prefix}(?:{'|'.join(parts)})?"
				patterns.append(result)
			else:
				patterns.append(prefix)
		self.hero_regex = f"(?:{'|'.join(patterns)})"


		for category in self.hero_stat_categories:
			for stat in category["stats"]:
				if "lambda" in stat:
					stat["lambda"] = eval(stat["lambda"])
		all_heroes = session.query(Hero).all()
		self.leveled_hero_stats.append(0)
		for level in range(1, 31):
			all_hero_stats = []
			for hero in all_heroes:
				hero_stats = {} #vars(hero)
				hero_stats["id"] = hero.id
				hero_stats["level"] = level
				for category in self.hero_stat_categories:
					for stat in category["stats"]:
						if "lambda" in stat:
							value = stat["lambda"](hero, hero_stats)
							hero_stats[stat["stat"]] = value
						else:
							hero_stats[stat["stat"]] = vars(hero)[stat["stat"]]
				all_hero_stats.append(hero_stats)
			self.leveled_hero_stats.append(all_hero_stats)

	def get_wiki_url(self, obj):
		if isinstance(obj, Hero):
			wikiurl = obj.localized_name
		elif isinstance(obj, Ability):
			wikiurl = f"{obj.hero.localized_name}#{obj.localized_name}"
		elif isinstance(obj, Item):
			wikiurl = obj.localized_name

		wikiurl = wikiurl.replace(" ", "_").replace("'", "%27")
		return f"http://dota2.gamepedia.com/{wikiurl}"

	def lookup_hero(self, hero):
		if not hero:
			return None
		if isinstance(hero, str):
			hero = hero.strip()
		hero_id = self.lookup_hero_id(hero)
		if hero_id:
			return session.query(Hero).filter(Hero.id == hero_id).first()
		else:
			return None

	def lookup_hero_id(self, text):
		if isinstance(text, int) or text.isdigit():
			query = session.query(Hero).filter(Hero.id == int(text))
			return int(text) if query.count() > 0 else None
		text = re.sub(r'[^a-z^\s]', r'', text.lower())
		if text == "":
			return None
		if text in self.hero_aliases:
			return self.hero_aliases[text]
		for hero in session.query(Hero):
			if hero.localized_name.lower().startswith(text):
				return hero.id
		for hero in self.hero_aliases:
			if hero.startswith(text):
				return self.hero_aliases[hero]
		for hero in self.hero_aliases:
			if text in hero:
				return self.hero_aliases[hero]
		return None

	def lookup_ability(self, text, full_check=True):
		if isinstance(text, str):
			text = text.strip()
		ability_query = session.query(Ability).filter(Ability.hero_id != None)
		if isinstance(text, int) or text.isdigit():
			return ability_query.filter(Ability.id == int(text)).first()
		def clean_input(t):
			return re.sub(r'[^a-z1-9\s]', r'', str(t).lower())
		text = clean_input(text)
		if text == "":
			return None
		for ability in ability_query:
			if clean_input(ability.localized_name) == text:
				return ability
		if full_check:
			for ability in ability_query:
				cleaned_name = clean_input(ability.localized_name)
				if cleaned_name.startswith(text):
					return ability
				cleaned_name = cleaned_name.replace(" ", "")
				if cleaned_name == text.replace(" ", ""):
					return ability
			for ability in ability_query:
				name = clean_input(ability.localized_name)
				if " " in name:
					for part in name.split(" "):
						if part.startswith(text):
							return ability
			for key in text.split(" "):
				if key in ABILITY_KEY_MAP:
					text = re.sub(f'\\b{key}\\b', '', text)
					hero = self.lookup_hero(text)
					if hero is None:
						return None
					ability_position = ABILITY_KEY_MAP[key]
					# use this instead of directly using ability_slot because there are some filler generic_ability things
					abilities = hero.abilities
					if ability_position > len(abilities):
						raise UserError(f"{hero.localized_name} doesn't have that many abilities")
					if key == "r": # if is ultimate and not invoker, get last ability in list
						def filter_ulti(ability):
							for bad_behavior in [ "not_learnable", "hidden" ]:
								if bad_behavior in (ability.behavior or ""):
									return False
							return True
						abilities = list(filter(filter_ulti, abilities))
						ability_position = len(abilities)
					return abilities[ability_position - 1]
		return None

	def lookup_item(self, item, full_check=True):
		if not item:
			return None
		if isinstance(item, str):
			item = item.strip()
		item_id = self.lookup_item_id(item, full_check)
		if item_id:
			return session.query(Item).filter(Item.id == item_id).first()
		else:
			return None

	def lookup_item_id(self, text, full_check=True):
		item_query = session.query(Item)
		if "recipe" not in text.lower():
			item_query = item_query.filter(~Item.localized_name.contains("recipe"))
			item_query = item_query.filter(~Item.localized_name.contains("Recipe"))
		if isinstance(text, int) or text.isdigit():
			return int(text)
		def clean_input(t):
			return re.sub(r'[^a-z1-9\s]', r'', str(t).lower())
		text = clean_input(text)
		if text == "":
			return None
		for item in item_query:
			if clean_input(item.localized_name) == text:
				return item.id
		if text in self.item_aliases:
			return self.item_aliases[text]

		if full_check:
			for item in self.item_aliases:
				if item.startswith(text):
					return self.item_aliases[item]
			for item in self.item_aliases:
				if text in item:
					return self.item_aliases[item]
		return None

	def lookup_patch(self, patch_name):
		query = session.query(Patch).filter(Patch.number == patch_name)
		if query.count() > 0:
			return query.first()
		else:
			return None

	def lookup_nth_patch(self, n):
		query = session.query(Patch).order_by(desc(Patch.timestamp))
		if n == 1: 
			# assume user wants latest MAJOR patch
			for patch in query:
				if not re.search(r"[a-zA-Z]", patch.number):
					return patch
		if n > query.count() or n < 0:
			return None
		else:
			return query.all()[n - 1]

	def lookup_patch_bounds(self, patch_name):
		query = session.query(Patch).order_by(Patch.timestamp)
		start = None
		end = None

		for patch in query:
			if start is None:
				if patch.number == patch_name:
					start = patch.timestamp
			else:
				if re.sub(r"[a-z]", "", patch.number) != patch_name:
					end = patch.timestamp
					break
		if end is None:
			end = datetime.datetime.now()

		return (start, end)


	def get_hero_infos(self):
		result = {}
		for hero in session.query(Hero):
			result[hero.id] = {
				"name": hero.localized_name,
				"full_name": hero.full_name,
				"icon": self.vpkurl + hero.icon,
				"attr": hero.attr_primary,
				"portrait": self.vpkurl + hero.portrait,
				"image": self.vpkurl + hero.image,
				"emoji": str(self.get_emoji(f"dota_hero_{hero.name}")),
				"roles": dict(zip(hero.roles.split("|"), map(int, hero.role_levels.split("|"))))
			}
			# role_values = list(map(int, hero.role_levels.split("|")))
			# rv_sum = sum(role_values)
			# role_values = list(map(lambda x: x / rv_sum, role_values))
			# result[hero.id]["roles"] = dict(zip(hero.roles.split("|"), role_values))
		result[0] = {
			"name": "Unknown",
			"full_name": "unknown_hero",
			"icon": self.vpkurl + "/panorama/images/heroes/icons/npc_dota_hero_antimage_png.png",
			"attr": "strength",
			"portrait": self.vpkurl + "/panorama/images/heroes/selection/npc_dota_hero_default_png.png",
			"image": self.vpkurl + "/panorama/images/heroes/npc_dota_hero_default_png.png",
			"emoji": "unknown_hero",
			"roles": {}
		}
		return result

	def get_item_infos(self):
		result = {}
		for item in session.query(Item):
			if item.icon is None:
				continue
			result[item.id] = {
				"name": item.localized_name,
				"icon": self.vpkurl + item.icon,
			}
		return result

	def get_ability_infos(self):
		result = {}
		for ability in session.query(Ability):
			if ability.icon is None:
				continue
			result[ability.id] = {
				"name": ability.localized_name,
				"icon": self.vpkurl + ability.icon,
				"slot": ability.slot,
				"entity": ability
			}
		return result

	def get_chat_wheel_infos(self):
		result = {}
		for message in session.query(ChatWheelMessage):
			result[message.id] = {
				"name": message.name,
				"message": message.message if message.message else message.name.replace("_", " ") + " (spray)",
				"is_sound": message.sound != None,
				"sound": self.vpkurl + message.sound if message.sound else None
			}
		return result

	def get_chatwheel_sound_clip(self, text):
		message = self.get_chatwheel_sound(text)
		if message:
			return f"dotachatwheel:{message.id}"
		else:
			return None

	def get_chatwheel_sound(self, text, loose_fit=False):
		def simplify(t):
			t = re.sub(r"[?!',！？.-]", "", t.lower())
			return re.sub(r"[_，]", " ", t)
		text = simplify(text)
		if text == "":
			return None
		if text.isdigit():
			query = session.query(ChatWheelMessage).filter_by(id=int(text))
			if query.count() > 0:
				return query.first()

		for message in session.query(ChatWheelMessage):
			if message.sound:
				strings = list(map(simplify, [ message.name, message.message, message.label ]))
				if text in strings:
					return message
				if loose_fit:
					for string in strings:
						if text.replace(" ", "") == string.replace(" ", ""):
							return message
					for string in strings:
						if text in string:
							return message
		return None

	async def play_response(self, response, ctx):
		await self.play_clip(f"dota:{response.fullname}", ctx)

	# used for getting the right response for dota clips
	def get_response(self, responsename):
		response = session.query(Response).filter(Response.fullname == responsename).first()
		if response:
			return response
		# to support legacy clips that used name instead of fullname
		return session.query(Response).filter(Response.name == responsename).first()

	# Plays a random response from a query
	async def play_response_query(self, query, ctx):
		await self.play_response(query.order_by(func.random()).first(), ctx)

	@commands.command(aliases=["dotar"])
	async def dota(self, ctx, *, keyphrase : str=None):
		"""Plays a dota response

		First tries to match the keyphrase with the name of a response

		If there is no response matching the input string, searches for any response that has the input string as part of its text

		To specify a specific hero to search for responses for, use ';' before the hero's name like this:
		`{cmdpfx}dota ;rubick`

		To specify a specific criteria to search for responses for, use ';' before the criteria name like this:
		`{cmdpfx}dota ;rubick ;defeat`
		There are some aliases for heroes, so the following will work:
		`{cmdpfx}dota sf`
		`{cmdpfx}dota furion`
		`{cmdpfx}dota shredder`

		If failing all of the above, the command will also try to find unlabeled heroes and critera. try:
		`{cmdpfx}dota juggernaut bottling`
		A few critera you can use are: kill, bottling, cooldown, acknowledge, immortality, nomana, and select

		To search for a response without asking mangobyte, try using the [Response Searcher](http://dotabase.dillerm.io/responses/)
		ProTip: If you click the discord button next to the response in the above web app, it will copy to your clipboard in the format needed to play using the bot."""
		query = await self.dota_keyphrase_query(keyphrase)

		if query is None:
			await ctx.send("No responses found! 😱")
		else:
			await self.play_response_query(query, ctx)


	async def dota_keyphrase_query(self, keyphrase):
		variables = [
			QueryVariable("hero", self.hero_aliases, lambda query, value: query.filter(Response.hero_id == value)),
			QueryVariable("criteria", self.criteria_aliases, lambda query, value: query.filter(or_(Response.criteria.like(value + "%"), Response.criteria.like("%|" + value + "%")))),
		]

		if keyphrase is None:
			words = []
		else:
			keyphrase = keyphrase.lower()
			words = keyphrase.split(" ")

		extract_var_prefix(words, variables)

		query = await self.smart_dota_query(words, variables)

		while query is None and extract_var(words, variables):
			query = await self.smart_dota_query(words, variables)

		return query


	async def smart_dota_query(self, words, variables, exact=False):
		basequery = session.query(Response)
		for var in variables:
			if var.value is not None:
				basequery = var.apply_filter(basequery)

		keyphrase = " ".join(words)

		if keyphrase == None or keyphrase == "" or keyphrase == " ":
			if basequery.count() > 0:
				return basequery
			else:
				return None

		# Because some of wisp's responses are not named correctly
		if '_' in keyphrase:
			query = basequery.filter(Response.name == keyphrase)
			if query.count() > 0:
				return query

		simple_input = " " + re.sub(r'[^a-z0-9\s]', r'', keyphrase.lower()) + " "

		query = basequery.filter(Response.text_simple == simple_input)
		if query.count() > 0:
			return query

		if not exact:
			query = basequery.filter(Response.text_simple.like("%" + simple_input + "%"))
			if query.count() > 0:
				return query

		return None

	async def get_laugh_response(self, hero=None):
		query = session.query(Response)
		if hero is not None:
			hero = self.lookup_hero(hero)
			query = query.filter(Response.hero_id == hero.id)
		query = query.filter(Response.criteria.like(f"%|HeroChatWheel%"))
		query = query.filter(Response.criteria.like(f"%IsEmoteLaugh%"))
		query = query.order_by(func.random())

		return query.first()

	@commands.command(aliases=["hi"])
	async def hello(self, ctx):
		"""Says hello

		WHAT MORE DO YOU NEED TO KNOW!?!?!? IS 'Says hello' REALLY NOT CLEAR ENOUGH FOR YOU!?!!11?!!?11!!?!??"""
		dota_hellos = [
			"slark_attack_11",
			"kunk_thanks_02",
			"meepo_scepter_06",
			"puck_ability_orb_03",
			"tink_spawn_07",
			"treant_ally_08",
			"wraith_lasthit_02",
			"timb_deny_08",
			"tech_pain_39",
			"meepo_attack_08",
			"slark_lasthit_02"
		]
		dota_response = random.choice(dota_hellos)
		response = session.query(Response).filter(Response.name == dota_response).first()
		logger.info("hello: " + response.name)
		await self.play_response(response, ctx)

	# Plays the correct command for the given keyphrase and hero, if a valid one is given
	async def hero_keyphrase_command(self, keyphrase, hero, ctx):
		query = await self.dota_keyphrase_query(keyphrase)
		if hero is None:
			await self.play_response_query(query, ctx)
			return

		hero = self.lookup_hero(hero)
		if hero is None:
			raise UserError("Don't know what hero yer talkin about")
		else:
			query = query.filter(Response.hero_id == hero.id)
			if query.count() > 0:
				await self.play_response_query(query, ctx)
			else:
				raise UserError(f"No responses found for {hero.localized_name}! 😱")

	@commands.command(aliases=["nope"])
	async def no(self, ctx, *, hero=None):
		"""Nopes."""
		await self.hero_keyphrase_command("no", hero, ctx)

	@commands.command()
	async def yes(self, ctx, *, hero=None):
		"""Oooooh ya."""
		await self.hero_keyphrase_command("yes", hero, ctx)

	@commands.command(aliases=["laugh", "haha", "lerl"])
	async def lol(self, ctx, *, hero=None):
		"""WOW I WONDER WAT THIS DOES

		Laughs using dota. Thats what it does."""
		response = await self.get_laugh_response(hero)
		await self.play_response(response, ctx)

	@commands.command(aliases=["ty"])
	async def thanks(self, ctx, *, hero=None):
		"""Gives thanks

		Thanks are given by a random dota hero in their own special way"""
		await self.hero_keyphrase_command(";thanks", hero, ctx)

	@commands.command()
	async def inthebag(self, ctx, *, hero=None):
		"""Proclaims that 'IT' (whatever it is) is in the bag"""
		query = await self.dota_keyphrase_query(";inthebag")
		if hero is None:
				await self.play_response_query(query.filter(Response.text_simple != " its in the bag "), ctx)
		else:
			hero = self.lookup_hero(hero)
			if hero is None:
				raise UserError("Don't know what hero yer talkin about")
			query = query.filter(Response.hero_id == hero.id)
			newquery = query.filter(Response.text_simple != " its in the bag ")
			if newquery.count() > 0:
				await self.play_response_query(newquery, ctx)
			else:
				await self.play_response_query(query, ctx)

	@commands.command()
	async def chatwheel(self, ctx, *, text):
		"""Plays the given chat wheel sound

		Give the command a number between 1 and 22 to page through all of the available chat options:
		`{cmdpfx}chatwheel 1`
		`{cmdpfx}chatwheel 3`

		**Examples:**
		`{cmdpfx}chatwheel Lakad Matataaaag!`
		`{cmdpfx}chatwheel disastah`
		`{cmdpfx}chatwheel Wan Bu Liao La`
		`{cmdpfx}chatwheel 玩不了啦`
		`{cmdpfx}chatwheel ehto gg`
		`{cmdpfx}chatwheel Это ГГ`"""
		page_size = 10
		query = session.query(ChatWheelMessage).filter(ChatWheelMessage.sound.like("/%"))
		if text.isdigit():
			page = int(text)
			max_pages = int((query.count() / page_size) + 1)
			if page < 1 or page > max_pages:
				raise UserError(f"Gotta give me a number between 1 and {max_pages}")
			sounds = []
			for message in query.offset((page - 1) * page_size).limit(page_size):
				if message.sound:
					sounds.append(f"{self.get_emoji('chat_wheel_sound')} {message.message}")
			embed = disnake.Embed(description="\n".join(sounds))
			embed.set_author(name=f"Chat Wheel Sounds ({page}/{max_pages})")
			await ctx.send(embed=embed)
			return

		message = self.get_chatwheel_sound(text, True)
		if message is None:
			raise UserError(f"Couldn't find chat wheel sound '{text}'")

		await self.play_clip(f"dotachatwheel:{message.id}", ctx)

	@commands.command()
	async def hero(self, ctx, *, hero : str):
		"""Gets information about a specific hero

		You can give this command almost any variant of the hero's name, or the hero's id

		**Examples:**
		`{cmdpfx}hero sf`
		`{cmdpfx}hero inker`
		`{cmdpfx}hero furi`"""
		hero = self.lookup_hero(hero)
		if not hero:
			raise UserError("That doesn't look like a hero")

		description = ""
		def add_attr(name, base_func, gain_func):
			global description
			result = f"{base_func(hero)} + {gain_func(hero)}"
			if hero.attr_primary == name:
				result = f"**{result}**"
			icon = self.get_emoji(f"attr_{name}")
			return f"{icon} {result}\n"

		description += add_attr("strength", lambda h: h.attr_strength_base, lambda h: h.attr_strength_gain)
		description += add_attr("agility", lambda h: h.attr_agility_base, lambda h: h.attr_agility_gain)
		description += add_attr("intelligence", lambda h: h.attr_intelligence_base, lambda h: h.attr_intelligence_gain)

		embed = disnake.Embed(description=description)

		if hero.color:
			embed.color = disnake.Color(int(hero.color[1:], 16))

		wikiurl = self.get_wiki_url(hero)

		embed.set_author(name=hero.localized_name, icon_url=f"{self.vpkurl}{hero.icon}", url=wikiurl)
		embed.set_thumbnail(url=f"{self.vpkurl}{hero.portrait}")

		base_damage = {
			"strength": hero.attr_strength_base,
			"agility": hero.attr_agility_base,
			"intelligence": hero.attr_intelligence_base
		}[hero.attr_primary]

		attack_stats = (
			f"{self.get_emoji('hero_damage')} {base_damage + hero.attack_damage_min} - {base_damage + hero.attack_damage_max}\n"
			f"{self.get_emoji('hero_attack_rate')} {hero.attack_rate}\n"
			f"{self.get_emoji('hero_attack_range')} {hero.attack_range}\n")
		if not hero.is_melee:
			attack_stats += f"{self.get_emoji('hero_projectile_speed')} {hero.attack_projectile_speed:,}\n"
		embed.add_field(name="Attack", value=attack_stats)

		base_armor = hero.base_armor + round(hero.attr_agility_base / 6.0, 1)
		embed.add_field(name="Defence", value=(
			f"{self.get_emoji('hero_armor')} {base_armor:0.1f}\n"
			f"{self.get_emoji('hero_magic_resist')} {hero.magic_resistance}%\n"))

		embed.add_field(name="Mobility", value=(
			f"{self.get_emoji('hero_speed')} {hero.base_movement}\n"
			f"{self.get_emoji('hero_turn_rate')} {hero.turn_rate}\n"
			f"{self.get_emoji('hero_vision_range')} {hero.vision_day:,} / {hero.vision_night:,}\n"))

		if hero.real_name != '':
			embed.add_field(name="Real Name", value=hero.real_name)

		roles = hero.roles.split("|")
		embed.add_field(name=f"Role{'s' if len(roles) > 1 else ''}", value=', '.join(roles))

		await ctx.send(embed=embed)

		query = session.query(Response).filter(Response.hero_id == hero.id).filter(or_(Response.criteria.like("Spawn %"), Response.criteria.like("Spawn%")))
		if query.count() > 0:
			try:
				await self.play_response_query(query, ctx)
			except AudioPlayerNotFoundError:
				pass

	@commands.command()
	async def talents(self, ctx, *, hero : str):
		"""Gets the talents of a specific hero

		You can give this command almost any variant of the hero's name, or the hero's id, in the same format as `{cmdpfx}hero`

		**Examples:**
		`{cmdpfx}talents shadow fiend`"""
		hero = self.lookup_hero(hero)
		if not hero:
			raise UserError("That doesn't look like a hero")

		image = await drawdota.draw_hero_talents(hero)
		image = disnake.File(image, f"{hero.name}_talents.png")

		await ctx.send(file=image)


	@commands.command(aliases=["spell"])
	async def ability(self, ctx, *, ability : str):
		"""Gets information about a specific hero ability

		**Examples:**
		`{cmdpfx}ability rocket flare`
		`{cmdpfx}ability laser`
		`{cmdpfx}ability sprout`"""

		ability = self.lookup_ability(ability)

		if ability is None:
			raise UserError("I couldn't find an ability by that name")

		def format_values(values):
			values = values.split(" ")
			return " / ".join(values)

		description = ""

		ability_behavior = OrderedDict([])
		ability_behavior["channelled"] = "Channelled"
		ability_behavior["autocast"] = "Auto-Cast"
		ability_behavior["unit_target"] = "Unit Target"
		ability_behavior["point"] = "Point Target"
		ability_behavior["toggle"] = "Toggle"
		ability_behavior["aura"] = "Aura"
		ability_behavior["passive"] = "Passive"
		ability_behavior["no_target"] = "No Target"

		if ability.behavior:
			behavior = ability.behavior.split("|")
			for key in ability_behavior:
				if key in behavior:
					extra_stuff = ""
					if "aoe" in behavior:
						extra_stuff = f" (AOE)"
					description += f"**Ability:** {ability_behavior[key]}{extra_stuff}\n"
					break

		if ability.damage_type:
			damage_type = ability.damage_type[0].upper() + ability.damage_type[1:]
			description += f"**Damage Type:** {damage_type}\n"

		if ability.spell_immunity:
			spell_immunity = ability.spell_immunity[0].upper() + ability.spell_immunity[1:]
			description += f"**Pierces Spell Immunity:** {spell_immunity}\n"

		if ability.dispellable:
			dispellable = {
				"yes": "Yes",
				"no": "No",
				"yes_strong": "Strong Dispells Only"
			}[ability.dispellable]
			description += f"**Dispellable:** {dispellable}\n"


		if description != "":
			description += "\n"

		description += ability.description

		ability_special = json.loads(ability.ability_special, object_pairs_hook=OrderedDict)
		attribute_additions = [
			{
				"key": "damage",
				"header": "Damage:",
				"value": ability.damage,
				"first": True
			},
			{
				"key": "channel_time",
				"header": "Channel Time:",
				"value": ability.channel_time
			},
			{
				"key": "cast_range",
				"header": "Cast Range:",
				"value": ability.cast_range if ability.cast_range != 0 else None
			},
			{
				"key": "cast_point",
				"header": "Cast Point:",
				"value": ability.cast_point
			}
		]
		for attr in attribute_additions:
			attribute = next((x for x in ability_special if (x.get("header") and format_pascal_case(x.get("header"))) == attr["header"]), None)
			if attribute:
				attribute["first"] = attr.get("first")
				if attribute.get("value", "") == "" and attr["value"] is not None:
					attribute["value"] = attr["value"]
			else:
				if attr["value"] is not None:
					ability_special.append(attr)
		first_attr = next((x for x in ability_special if x.get("first")), None)
		if first_attr:
			ability_special.remove(first_attr)
			ability_special.insert(0, first_attr)

		formatted_attributes = []
		scepter_attributes = []
		shard_attributes = []
		for attribute in ability_special:
			header = attribute.get("header")
			if not header:
				continue
			header = format_pascal_case(header)

			value = attribute["value"]
			footer = attribute.get("footer")
			text = f"**{header}** {format_values(value)}"
			if footer:
				text += f" {footer}"

			if attribute.get("scepter_upgrade") and not ability.scepter_grants:
				scepter_attributes.append(text)
			elif attribute.get("shard_upgrade") and not ability.shard_grants:
				shard_attributes.append(text)
			else:
				formatted_attributes.append(text)

		if formatted_attributes:
			description += "\n\n" + "\n".join(formatted_attributes)

		# talents
		talent_query = query_filter_list(session.query(Talent), Talent.linked_abilities, ability.name)
		talents = talent_query.order_by(Talent.slot).all()
		if len(talents) > 0:
			description += f"\n\n{self.get_emoji('talent_tree')} **Talents:**"
			for talent in talents:
				description += f"\n[Level {talent.level}] {talent.localized_name}"

		# aghs scepter
		if ability.scepter_description:
			if ability.scepter_grants:
				description += f"\n\n{self.get_emoji('aghanims_scepter')} **Granted by Aghanim's Scepter**"
			else:
				description += f"\n\n{self.get_emoji('aghanims_scepter')} __**Upgradable by Aghanim's Scepter**__\n"
				description += f"*{ability.scepter_description}*\n"
				for attribute in scepter_attributes:
					description += f"\n{attribute}"

		# aghs shard
		if ability.shard_description:
			if ability.shard_grants:
				description += f"\n\n{self.get_emoji('aghanims_shard')} **Granted by Aghanim's Shard**"
			else:
				description += f"\n\n{self.get_emoji('aghanims_shard')} __**Upgradable by Aghanim's Shard**__\n"
				description += f"*{ability.shard_description}*\n"
				for attribute in shard_attributes:
					description += f"\n{attribute}"

		embed = disnake.Embed(description=description)

		embed.title = ability.localized_name
		embed.url = self.get_wiki_url(ability)

		embed.set_thumbnail(url=f"{self.vpkurl}{ability.icon}")

		if ability.cooldown and ability.cooldown != "0":
			value = format_values(ability.cooldown)
			if ability.charges:
				value += f" ({ability.charges} Charges)"
			embed.add_field(name="\u200b", value=f"{self.get_emoji('cooldown')} {value}\n")

		if ability.mana_cost and ability.mana_cost != "0":
			embed.add_field(name="\u200b", value=f"{self.get_emoji('mana_cost')} {format_values(ability.mana_cost)}\n")

		if ability.lore and ability.lore != "":
			embed.set_footer(text=ability.lore)

		await ctx.send(embed=embed)

	@commands.command()
	async def item(self, ctx, *, item : str):
		"""Gets information about a specific item

		**Examples:**
		`{cmdpfx}item shadow blade`
		`{cmdpfx}item tango`"""

		item = self.lookup_item(item)

		if item is None:
			raise UserError("I couldn't find an item by that name")

		description = ""

		if item.neutral_tier is not None:
			description += f"**Tier {item.neutral_tier}** Neutral Item\n\n"


		def format_values(values, join_string="/", base_level=None):
			if values is None:
				return None
			values = values.split(" ")
			if base_level and base_level <= len(values):
				values[base_level - 1] = f"**{values[base_level - 1]}**"
			else:
				values = map(lambda v: f"**{v}**", values)
			return join_string.join(values)

		ability_special = json.loads(item.ability_special, object_pairs_hook=OrderedDict)
		for attribute in ability_special:
			header = attribute.get("header")
			if not header:
				continue
			value = attribute["value"]
			footer = attribute.get("footer")
			text = f"{header} {format_values(value, base_level=item.base_level)}"
			if footer:
				text += f" {footer}"
			text += "\n"
			description += text


		if item.description:
			if description != "":
				description += "\n"
			description += item.description
			description += "\n"
		description = re.sub(r"(^|\n)# ([^\n]+)\n", r"\n__**\2**__\n", description)

		def clean_values(values):
			values = values.split(" ")
			return " / ".join(values)

		description += "\n"
		if item.cost and item.cost != "0":
			description += f"{self.get_emoji('gold')} {item.cost:,}\n"
		if item.mana_cost and item.mana_cost != "0":
			description += f"{self.get_emoji('mana_cost')} {clean_values(item.mana_cost)}  "
		if item.cooldown and item.cooldown != "0":
			description += f"{self.get_emoji('cooldown')} {clean_values(item.cooldown)}"

		embed = disnake.Embed(description=description)

		color = drawdota.get_item_color(item)
		if color is not None:
			embed.color = disnake.Color(int(color[1:], 16))


		embed.title = item.localized_name
		embed.url = self.get_wiki_url(item)

		embed.set_thumbnail(url=f"{self.vpkurl}{item.icon}")

		if item.lore and item.lore != "":
			embed.set_footer(text=item.lore)

		await ctx.send(embed=embed)


	@commands.command(aliases=["emoji"])
	async def emoticon(self, ctx, name):
		"""Gets the gif of a dota emoticon

		<a:pup:406270527766790145> <a:stunned:406274986769252353> <a:cocky:406274999951949835>

		**Examples:**
		`{cmdpfx}emoticon pup`
		`{cmdpfx}emoticon stunned`
		`{cmdpfx}emoticon naga_song`"""
		await ctx.channel.trigger_typing()

		emoticon = session.query(Emoticon).filter(Emoticon.name == name).first()

		if not emoticon:
			raise UserError(f"Couldn't find an emoticon with the name '{name}'")

		url = self.vpkurl + emoticon.url

		filetype = "gif" if emoticon.frames > 1 else "png"
		image = disnake.File(await drawdota.create_dota_emoticon(emoticon, url), f"{name}.{filetype}")

		await ctx.send(file=image)

	@commands.command(aliases=["addemoji"])
	async def addemoticon(self, ctx, name):
		"""Adds a dota emoticon as an animated emoji

		This command will add the dota emoticon as an animated emoji to the server. Because it is an animated emoji, only discord nitro users will be able to use it.

		Obviously, this command needs the 'Manage Emoji' permission to be able to work.

		<a:pup:406270527766790145> <a:stunned:406274986769252353> <a:cocky:406274999951949835>

		**Examples:**
		`{cmdpfx}addemoticon pup`
		`{cmdpfx}addemoticon stunned`
		`{cmdpfx}addemoticon naga_song`"""

		emoticon = session.query(Emoticon).filter(Emoticon.name == name).first()

		if not emoticon:
			raise UserError(f"Couldn't find an emoticon with the name '{name}'")

		url = self.vpkurl + emoticon.url
		image = await drawdota.create_dota_emoticon(emoticon, url)
		with open(image, 'rb') as f:
			image = f.read()

		if not ctx.guild:
			raise UserError("You have to be in a server to use this command")

		if not ctx.guild.me.guild_permissions.manage_emojis:
			raise UserError("An admin needs to give me the 'Manage Emojis' permission before I can do that")

		await ctx.guild.create_custom_emoji(name=name, image=image, reason=f"Dota emoji created for {ctx.message.author.name}")

		await ctx.message.add_reaction("✅")

	@commands.command()
	async def lore(self, ctx, *, name=None):
		"""Gets the lore of a hero, ability, or item

		Returns a random piece of lore if no name is specified

		**Examples:**
		`{cmdpfx}lore bristleback`
		`{cmdpfx}lore shadow blade`
		`{cmdpfx}lore venomous gale`"""
		lore_info = {}
		found = False

		if name is None:
			# Randomize!
			names = []
			for item in session.query(Item).filter(Item.lore != ""):
				names.append(item.localized_name)
			for ability in session.query(Ability).filter(Ability.lore != ""):
				names.append(ability.localized_name)
			for hero in session.query(Hero).filter(Hero.bio != ""):
				names.append(hero.localized_name)
			name = random.choice(names)

		item = self.lookup_item(name, False)
		if item:
			found = True
			lore_info = {
				"name": item.localized_name,
				"icon": item.icon,
				"lore": item.lore,
				"object": item
			}

		if not found:
			ability = self.lookup_ability(name, False)
			if ability:
				found = True
				lore_info = {
					"name": ability.localized_name,
					"icon": ability.icon,
					"lore": ability.lore,
					"object": ability
				}

		if not found:
			hero = self.lookup_hero(name)
			if hero:
				found = True
				lore_info = {
					"name": hero.localized_name,
					"icon": hero.portrait,
					"lore": hero.bio,
					"object": hero
				}

		if not found:
			raise UserError("I Couldn't find an ability hero or item by that name")

		if lore_info["lore"] == "":
			raise UserError("There is no in-game lore for that")


		lore_text = lore_info["lore"]
		maxlen = 1950
		if len(lore_text) > maxlen:
			lore_text = lore_text[:maxlen] + "..."
		embed = disnake.Embed(description=lore_text)

		embed.title = lore_info["name"]
		embed.url = self.get_wiki_url(lore_info["object"])

		if lore_info["icon"]:
			embed.set_thumbnail(url=f"{self.vpkurl}{lore_info['icon']}")

		await ctx.send(embed=embed)

	@commands.command(aliases=["aghs", "ags", "aghanims", "scepter", "shard"])
	async def aghanim(self, ctx, *, name):
		"""Gets the aghs upgrade for the given hero or ability

		This command will get the information about shard upgrades AND scepter upgrades.
		If you want just shard or just scepter upgrades, try using `{cmdpfx}scepter` or `{cmdpfx}shard`"""
		only_do_scepter = ctx.invoked_with == "scepter"
		only_do_shard = ctx.invoked_with == "shard"

		abilities = []
		hero = self.lookup_hero(name)
		if hero:
			for ability in hero.abilities:
				if (ability.shard_upgrades or ability.shard_grants) and not only_do_scepter:
					abilities.append(ability)
				elif (ability.scepter_upgrades or ability.scepter_grants) and not only_do_shard:
					abilities.append(ability)

			if len(abilities) == 0:
				raise UserError(f"Couldn't find an aghs upgrade for {hero.localized_name}. Either they don't have one or I just can't find it.")
		else:
			ability = self.lookup_ability(name, True)
			if not ability:
				raise UserError("Couldn't find a hero or ability by that name")
			abilities = [ ability ]

		item_shard = self.lookup_item("aghanim's shard")
		item_scepter = self.lookup_item("aghanim's scepter")
		upgrade_types = [ "scepter", "shard" ]
		if only_do_scepter:
			upgrade_types = [ "scepter" ]
		elif only_do_shard:
			upgrade_types = [ "shard" ]

		for upgrade_type in upgrade_types:
			aghs_item = item_scepter
			icon_url = f"{self.vpkurl}/panorama/images/hud/reborn/aghsstatus_scepter_on_psd.png"
			if upgrade_type == "shard":
				aghs_item = item_shard
				icon_url = f"{self.vpkurl}/panorama/images/hud/reborn/aghsstatus_shard_on_psd.png"
			for ability in abilities:
				description = ability.scepter_description if upgrade_type == "scepter" else ability.shard_description
				is_grantedby = ability.scepter_grants if upgrade_type == "scepter" else ability.shard_grants
				if description != "":
					if is_grantedby:
						description = f"**{description}**\n\n*{ability.description}*"
					else:
						description = f"*{description}*\n"

				ability_special = json.loads(ability.ability_special, object_pairs_hook=OrderedDict)
				formatted_attributes = []
				if upgrade_type == "scepter" and ability.scepter_upgrades and not ability.scepter_grants:
					for attribute in ability_special:
						header = attribute.get("header")
						if not (header and attribute.get("scepter_upgrade")):
							continue
						header = format_pascal_case(header)
						value = attribute["value"]
						footer = attribute.get("footer")
						value = " / ".join(value.split(" "))
						text = f"**{header}** {value}"
						if footer:
							text += f" {footer}"
						if description != "":
							description += "\n"
						description += f"{text}"

				if description == "":
					continue
				embed = disnake.Embed(description=description)
				title = f"{aghs_item.localized_name} ({ability.localized_name})"
				embed.set_author(name=title, icon_url=icon_url)
				embed.set_thumbnail(url=f"{self.vpkurl}{ability.icon}")
				await ctx.send(embed=embed)

	@commands.command(aliases=["recipes", "craft", "crafting"])
	async def recipe(self, ctx, *, item):
		"""Shows the recipes involving this item"""
		item = self.lookup_item(item, True)
		if not item:
			raise UserError("Can't find an item by that name")

		products = query_filter_list(session.query(Item), Item.recipe, item.name).all()
		components = []
		if item.recipe:
			component_names = item.recipe.split("|")
			found_components = session.query(Item).filter(Item.name.in_(component_names)).all()
			for name in component_names:
				for component in found_components:
					if component.name == name:
						components.append(component)
						break


		embed = disnake.Embed()

		embed.description = f"**Total Cost:** {self.get_emoji('gold')} {item.cost}"

		if components:
			value = ""
			for i in components:
				value += f"{i.localized_name} ({self.get_emoji('gold')} {i.cost})\n"
			embed.add_field(name="Created from", value=value)
		if products:
			value = ""
			for i in products:
				value += f"{i.localized_name} ({self.get_emoji('gold')} {i.cost})\n"
			embed.add_field(name="Can be made into", value=value)

		title = item.localized_name
		if len(products) > 1 or (components and products):
			title += " (Recipes)"
		else:
			title += " (Recipe)"

		embed.title = title
		embed.url = self.get_wiki_url(item)

		color = drawdota.get_item_color(item)
		if color is not None:
			embed.color = disnake.Color(int(color[1:], 16))

		image = disnake.File(await drawdota.draw_itemrecipe(item, components, products), "recipe.png")
		embed.set_image(url=f"attachment://{image.filename}")

		await ctx.send(embed=embed, file=image)



	@commands.command(aliases=["fuse", "fuze", "fuzeheroes"])
	async def fuseheroes(self, ctx, *, heroes=None):
		"""See what would happen if you fused two heroes together

		If no heroes are given, two will be chosen at random

		**Example:**
		`{cmdpfx}fuseheroes axe chen`"""
		await ctx.channel.trigger_typing()
		if heroes is None:
			heroes = session.query(Hero).order_by(func.random()).limit(2).all()
			heroes = " ".join(map(lambda h: h.localized_name, heroes))

		words = heroes.split(" ")

		hero1 = None
		hero2 = None
		for i in range(1, len(words)):
			hero1 = self.lookup_hero(" ".join(words[:i]))
			hero2 = self.lookup_hero(" ".join(words[i:]))
			if hero1 and hero2:
				break

		if not (hero1 and hero2):
			raise UserError("That doesn't look like two distinct heroes")
		if hero1.id == hero2.id:
			raise UserError("Fusing something with itself sounds boring")

		def combine_words(word1, word2):
			middle1 = len(word1) - (len(word1) // 2)
			middle2 = len(word2) - (len(word2) // 2)
			return word1[:middle1] + word2[middle2:]

		name1 = hero1.localized_name
		name2 = hero2.localized_name
		if " " not in name1 and " " not in name2:
			hero_name = combine_words(name1, name2)
		if " " in name1 and " " not in name2:
			hero_name = name1.split(" ")[0] + " " + name2
		if " " not in name1 and " " in name2:
			hero_name = name1 + " " + name2.split(" ")[-1]
		if " " in name1 and " " in name2:
			hero_name = name1.split(" ")[0] + " " + name2.split(" ")[-1]
			if hero_name == name1 or hero_name == name2:
				hero_name = combine_words(name1.split(" ")[0], name2.split(" ")[0]) + " " + name2.split(" ")[-1]
			if hero_name == name1 or hero_name == name2:
				hero_name = name1.split(" ")[0] + " " + combine_words(name1.split(" ")[-1], name2.split(" ")[-1])


		embed = disnake.Embed()

		embed.title = hero_name

		emoji1 = self.get_emoji(f"dota_hero_{hero1.name}")
		emoji2 = self.get_emoji(f"dota_hero_{hero2.name}")

		embed.description = f"{emoji1} + {emoji2}"

		color1 = imagetools.Color(hero1.color)
		color2 = imagetools.Color(hero2.color)
		color = color1.blend(color2)
		embed.color = disnake.Color(color.integer)

		image = disnake.File(await drawdota.fuse_hero_images(hero1, hero2), "hero.png")
		embed.set_thumbnail(url=f"attachment://{image.filename}")

		await ctx.send(embed=embed, file=image)


	@commands.command()
	async def courage(self, ctx, *, hero = None):
		"""Generates a challenge build

		Creates a challenge build with a random (or given) hero and a random set of items

		**Examples:**
		`{cmdpfx}courage`
		`{cmdpfx}courage shadow fiend`"""

		all_boots = query_filter_list(session.query(Item), Item.recipe, "item_boots").all()

		random.seed(datetime.datetime.now())
		items = session.query(Item) \
			.filter(~Item.localized_name.contains("Recipe")) \
			.filter(~Item.localized_name.contains("Boots")) \
			.filter(Item.recipe != None) \
			.filter(Item.icon != None) \
			.filter(Item.cost > 2000) \
			.order_by(func.random()) \
			.limit(5) \
			.all()
		items.append(random.choice(all_boots))
		random.shuffle(items)

		item_ids = []
		for item in items:
			item_ids.append(item.id)
		if hero:
			hero_id = self.lookup_hero_id(hero)
			if not hero_id:
				raise UserError(f"Couldn't a hero called '{hero}'")
		else:
			hero_id = session.query(Hero).order_by(func.random()).first().id

		logger.info(item_ids)

		image = disnake.File(await drawdota.draw_courage(hero_id, item_ids), "courage.png")
		await ctx.send(file=image)


	@commands.command(aliases=["neutrals", "neutraltier"])
	async def neutralitems(self, ctx, *, tier = None):
		"""Displays all of the neutral items

		If a tier is specified, display the items in that tier, along with their names

		`{cmdpfx}neutralitems`
		`{cmdpfx}neutralitems tier 5`
		`{cmdpfx}neutralitems 3`"""

		if tier is not None:
			tier = tier.lower().replace("tier", "").replace("t", "").strip()
			if not tier.isdigit():
				raise UserError("Please specify a tier like 'tier 5'")
			tier = int(tier)
			if tier < 1 or tier > 5:
				raise UserError("Please specify a tier between 1 and 5")

		embed = disnake.Embed()

		title = "Neutral Items"
		if tier is not None:
			title = f"Tier {tier} Neutral Items"
		embed.title = title
		embed.url = "https://dota2.gamepedia.com/Neutral_Items"

		all_neutral_items = session.query(Item).filter(Item.neutral_tier != None).filter(Item.recipe == None).order_by(Item.localized_name).all()
		image = disnake.File(await drawdota.draw_neutralitems(tier, all_neutral_items), "neutralitems.png")
		embed.set_image(url=f"attachment://{image.filename}")
		if tier is not None:
			tier_color = drawdota.neutral_tier_colors[str(tier)]
			embed.color = disnake.Color(int(tier_color[1:], 16))

		if tier is None:
			embed.set_footer(text="Also try: ?neutralitems tier 4")
		await ctx.send(embed=embed, file=image)

	@commands.command(aliases=["startingstats", "tradingstats", "lvlstats", "lvledstats"])
	async def leveledstats(self, ctx, *, hero : str):
		"""Gets the stats for a hero at the specified level

		If no level is specified, get the stats for the hero at level 1

		**Examples:**
		`{cmdpfx}leveledstats tinker`
		`{cmdpfx}leveledstats shaker lvl 2`
		`{cmdpfx}leveledstats level 28 shaman`"""
		lvl_regex = r"(?:(max) (?:lvl|level)|(?:lvl|level)? ?(\d+))"
		match = re.search(lvl_regex, hero, re.IGNORECASE)
		level = 1
		if match:
			if match.group(1):
				level = 30
			else:
				level = int(match.group(2))
			if level < 1 or level > 30:
				raise UserError("Please enter a level between 1 and 30")
			hero = re.sub(lvl_regex, "", hero)

		hero = self.lookup_hero(hero)
		if not hero:
			raise UserError("That doesn't look like a hero")

		stat_category = next((c for c in self.hero_stat_categories if c["section"] == "Combat Stats"), None)["stats"]

		description = ""
		hero_stats = next((h for h in self.leveled_hero_stats[level] if h["id"] == hero.id), None)

		for stat in stat_category:
			name = stat["name"]
			value = hero_stats[stat["stat"]]
			if stat.get("display") == "resistance_percentage":
				value = 100 * (1 - value)
			if stat.get("display") == "int":
				value = round(value)
			value = f"{value:.2f}"
			value = re.sub("\.0+$", "", value)
			if stat.get("display") == "resistance_percentage":
				value += "%"
			description += f"\n{name}: **{value}**"

		embed = disnake.Embed(description=description)

		title = f"Level {level} {hero.localized_name}"
		embed.set_author(name=title, icon_url=f"{self.vpkurl}{hero.icon}")
		embed.set_thumbnail(url=f"{self.vpkurl}{hero.portrait}")
		if hero.color:
			embed.color = disnake.Color(int(hero.color[1:], 16))
		embed.set_footer(text="The stats shown above do not account for talents, passives, or items")

		await ctx.send(embed=embed)

	@commands.command(aliases=["statstable", "stattable", "heroestable", "leveledstatstable", "besthero", "bestheroes"])
	async def herotable(self, ctx, *, table_args : HeroStatsTableArgs):
		"""Displays a sorted table of heroes and their stats

		Displays a table with computed hero stats showing which heroes have the highest values for the specified stat. To see the list of possible stats, try the `{cmdpfx}leveledstats` command

		**Examples:**
		`{cmdpfx}herotable dps`
		`{cmdpfx}herotable health lvl 30`
		`{cmdpfx}herotable attack speed level 21 descending`
		"""
		if table_args.stat is None:
			raise UserError(f"Please select a stat to sort by. For a list of stats, see `{self.cmdpfx(ctx)}leveledstats`")
		if table_args.hero_level < 1 or table_args.hero_level > 30:
			raise UserError("Please select a hero level between 1 and 30")
		if table_args.hero_count < 2 or table_args.hero_count > 40:
			raise UserError("Please select a hero count between 2 and 40")

		embed = disnake.Embed()

		image = disnake.File(await drawdota.draw_herostatstable(table_args, self.hero_stat_categories, self.leveled_hero_stats), "herotable.png")
		embed.set_image(url=f"attachment://{image.filename}")
		embed.set_footer(text="The stats shown above do not account for talents, passives, or items")

		await ctx.send(embed=embed, file=image)

	@commands.command(aliases=["spells"])
	async def abilities(self, ctx, *, hero):
		"""Shows all of the abilities/spells for that hero"""
		hero = self.lookup_hero(hero)
		if not hero:
			raise UserError("That doesn't look like a hero")

		abilities = []
		for ability in list(filter(lambda a: a.slot is not None, hero.abilities)):
			if not hero.id == 74: # invoker
				if "hidden" in ability.behavior and not (ability.shard_grants or ability.scepter_grants):
					continue
			abilities.append(ability)

		embed = disnake.Embed()

		embed.title = hero.localized_name
		embed.url = self.get_wiki_url(hero)

		image = disnake.File(await drawdota.draw_heroabilities(abilities), "abilities.png")
		embed.set_image(url=f"attachment://{image.filename}")

		embed.color = disnake.Color(int(hero.color[1:], 16))

		await ctx.send(embed=embed, file=image)


	@commands.command(aliases = ["rss"])
	async def blog(self,ctx):
		""" Pulls the newest blog post for Dota 2"""
		await ctx.send("Sorry, Valve broke this for now.")
		return # return cuz valve broke it
		feed = await httpgetter.get(r'https://blog.dota2.com/feed', return_type="text")
		blog = feedparser.parse(feed)
		title = "Dota 2 Blog"
		embed = rsstools.create_embed(title, blog.entries[0])
		await ctx.send(embed = embed)

	@tasks.loop(minutes=5)
	async def check_dota_blog(self):
		feed = await httpgetter.get(r'https://blog.dota2.com/feed', return_type="text")
		blog = feedparser.parse(feed)
		title = "Dota 2 Blog"

		updated = rsstools.is_new_blog(blog.entries[0])
		if not updated: #if its not updated, stop here
			return

		embed = rsstools.create_embed(title, blog.entries[0]) #generate embed

		##next section copies code in check_dota_patch in general cogs
		messageables = []
		#find channels to post in
		guildinfos = botdata.guildinfo_list()
		for guildinfo in guildinfos:
			if guildinfo.dotablogchannel is not None:
				channel = self.bot.get_channel(guildinfo.dotablogchannel)
				if channel is not None:
					messageables.append(channel)
				else:
					logger.info(f"couldn't find channel {guildinfo.dotablogchannel} when announcing dota blog")

		#find users
		userinfos = botdata.userinfo_list()
		for userinfo in userinfos:
			if userinfo.dmdotablog:
				user = self.bot.get_user(userinfo.discord)
				if user is not None:
					messageables.append(user)
				else:
					logger.info(f"couldn't find user {userinfo.discord} when announcing dota blog")

		#bundle tasks and execute
		tasks = []
		for messageable in messageables:
			tasks.append(messageable.send(embed=embed))

		bundler = AsyncBundler(tasks)
		await bundler.wait() class DotaStats(MangoCog):
	"""Commands for displaying information about Dota 2 players and matches

	Most of the data for this is collected through the [OpenDota API](https://docs.opendota.com/)"""

	def __init__(self, bot):
		MangoCog.__init__(self, bot)
		self.embed_color = disnake.Color.teal()
		dotabase = self.bot.get_cog("Dotabase")
		if not dotabase:
			raise ImportError("The Dotabase cog must be added before the DotaStats cog")
		self.dota_game_strings = read_json(settings.resource("json/dota_game_strings.json"))
		self.hero_info = dotabase.get_hero_infos()
		self.lookup_hero = dotabase.lookup_hero
		self.chat_wheel_info = dotabase.get_chat_wheel_infos()
		self.dota_gif_lock = asyncio.Lock()

	async def get_meta_json(self): 
		url = 'https://api.opendota.com/api/herostats'
		return await httpgetter.get(url)

	def sort_meta(self, json, num=10): 
		"""re-orders the meta json based on pick/ban + winrate.
		num = number of top heroes to include """
		total_games = get_total_pro_games(json)
		json = list(filter(lambda x: x.get('pro_pick', 1) > 0, json))
		sorted_json = sorted(
			json, 
			reverse=True, 
			# sorts by (winrate) + (pick/ban rate)
			key=lambda x: (((x.get('pro_pick', 0) + x.get('pro_ban', 0)) / total_games)
			+ ((x.get('pro_win', 0) / x.get('pro_pick', 1)))),
			)
		if num > 0: 
			return sorted_json[:num]
		return sorted_json

	async def print_meta(self, ctx, num_to_list): 
		"""prints the meta table"""
		json = await self.get_meta_json()
		sorted_json = self.sort_meta(json, num_to_list)
		description = (f"Top {num_to_list} meta hero(s) in professional matches")
		embed = disnake.Embed(description = description, color=self.embed_color)
		meta_table = disnake.File(await drawdota.draw_meta_table(sorted_json, json), "meta.png")
		embed.set_image(url=f"attachment://{meta_table.filename}")
		await ctx.send(embed=embed, file=meta_table)

	def get_pretty_hero(self, player, use_icons=False):
		dotabase = self.bot.get_cog("Dotabase")
		if player["hero_id"] not in self.hero_info:
			return "**Unknown**"
		name = self.hero_info[player["hero_id"]]["name"]
		if use_icons:
			emoji = self.hero_info[player["hero_id"]]["emoji"]
			return f"{emoji}**{name}**"
		return f"**{name}**"

	def get_player_rank(self, playerinfo):
		# gets the players rank information as a string with a rank emoticaon
		rank_strings = [ "Unranked", "Herald", "Guardian", "Crusader", "Archon", "Legend", "Ancient", "Divine", "Immortal" ]

		base_rank_tier = playerinfo.get("rank_tier")
		if base_rank_tier is None:
			base_rank_tier = 0
		rank_tier = base_rank_tier // 10
		leaderboard_rank = playerinfo.get("leaderboard_rank")
		rank_string = f"**{rank_strings[rank_tier]}**"
		stars = min(base_rank_tier % 10, 7)
		if stars > 0:
			rank_string += f" [{stars}]"
		on_leaderboard = rank_tier >= 7 and leaderboard_rank
		if on_leaderboard:
			rank_string = f"**Immortal** [Rank {leaderboard_rank}]"
			rank_tier = 8

		emoji_id = f"rank_{rank_tier}"
		if on_leaderboard:
			if leaderboard_rank <= 10:
				emoji_id += "c"
			elif leaderboard_rank <= 100:
				emoji_id += "b"

		rank_string = self.get_emoji(emoji_id) + " " + rank_string

		return rank_string

	async def get_player_mention(self, steamid, ctx):
		# expects that steamid is a valid int
		player = await DotaPlayer.convert(ctx, steamid)
		return player.mention

	async def create_dota_gif(self, match, stratz_match, start_time, end_time, ms_per_second=100):
		await self.dota_gif_lock.acquire()
		try:
			result = await drawdota.create_dota_gif(self.bot, match, stratz_match, start_time, end_time, ms_per_second)
		finally:
			self.dota_gif_lock.release()
		return result

	async def get_teamfights(self, game, is_radiant):
		teamfights = []
		for teamfight in game['teamfights']:
			net_gain = 0
			our_dead = []
			their_dead = []
			num_players = min([ len(teamfight['players']), len(game['players']) ])
			for i in range(0, num_players):
				deadtext = self.get_pretty_hero(game['players'][i])
				if teamfight['players'][i]['deaths'] == 0:
					deadtext = None
				elif teamfight['players'][i]['deaths'] > 1:
					deadtext += "(x{})".format(teamfight['players'][i]['deaths'])

				if (game['players'][i]['isRadiant'] == is_radiant): # on our team
					net_gain += teamfight['players'][i]['gold_delta']
					if deadtext:
						our_dead.append(deadtext)
				else:
					net_gain -= teamfight['players'][i]['gold_delta']
					if deadtext:
						their_dead.append(deadtext)
			teamfight_dict = {
					"gain_loss": "gain" if net_gain >= 0 else "loss",
					"our_dead": pretty_list(our_dead, None),
					"their_dead": pretty_list(their_dead, None),
					"net_change": abs(net_gain),
					"deaths": teamfight['deaths'],
					"time": teamfight['start'],
					"time_end": teamfight['end']
				}
			teamfight_dict['formatted'] = format_teamfight(teamfight_dict)
			teamfights.append(teamfight_dict)
		return teamfights

	async def get_firstblood_story(self, game, is_radiant):
		fb_objective = next((obj for obj in game['objectives'] if obj['type'] == "CHAT_MESSAGE_FIRSTBLOOD"), None)
		if fb_objective is None:
			return "" # No first blood this game, or it wasnt reported in objectives log
		fb_log = None
		fb_killer = next(p for p in game['players'] if p['player_slot'] == fb_objective['player_slot'])
		fb_log = next((kill for kill in fb_killer['kills_log'] if kill['time'] == fb_objective['time']), None)
		if fb_log is None:
			return "" # Can't find the kill log of when first blood happened
		dotabase = self.bot.get_cog("Dotabase")
		fb_victim_id = next(h for h in self.hero_info if self.hero_info[h]['full_name'] == fb_log['key'])
		fb_victim = next(p for p in game['players'] if p['hero_id'] == fb_victim_id)

		return "First blood was drawn when {} {} killed {} {} at {}\n\n".format(
			"our" if (fb_killer['isRadiant'] == is_radiant) else "their",
			self.get_pretty_hero(fb_killer),
			"our" if (fb_victim['isRadiant'] == is_radiant) else "their",
			self.get_pretty_hero(fb_victim),
			get_pretty_duration(fb_objective['time']))


	async def get_teamfight_stories(self, game, is_radiant):
		teamfights = await self.get_teamfights(game, is_radiant)
		teamfights_count = len(teamfights)
		story = ""

		timeline = []
		most_deaths_fights = 2
		most_change_fights = 2
		if len(teamfights) > most_deaths_fights + most_change_fights:
			# do calcs
			teamfights = sorted(teamfights, key=lambda t: t['net_change'], reverse=True)
			for i in range(0, most_change_fights):
				timeline.append(teamfights.pop(0))
			teamfights = sorted(teamfights, key=lambda t: t['deaths'], reverse=True)
			for i in range(0, most_deaths_fights):
				timeline.append(teamfights.pop(0))
		else:
			timeline.extend(teamfights)
			teamfights = []

		timeline = sorted(timeline, key=lambda t: t['time'])
		return list(map(lambda t: t["formatted"], timeline))

	async def get_lane_story(self, players, laneid, is_radiant, use_icons=False):
		our_eff = 0
		their_eff = 0
		our_heroes = []
		their_heroes = []
		for player in players:
			if player['lane'] == laneid and not player.get('is_roaming', False):
				if (player['isRadiant'] == is_radiant): #on our team
					if player.get('lane_efficiency', 0) > our_eff:
						our_eff = player['lane_efficiency']
					our_heroes.append(self.get_pretty_hero(player, use_icons))
				else: #on their team
					if player.get('lane_efficiency', 0) > their_eff:
						their_eff = player['lane_efficiency']
					their_heroes.append(self.get_pretty_hero(player, use_icons))
		return {
			"us": pretty_list(our_heroes, "An empty lane"),
			"won_lost": "won" if our_eff > their_eff else "lost",
			"them": pretty_list(their_heroes, "an empty lane")
		}

	# gets the story for all of the lanes
	async def get_lane_stories(self, game, is_radiant, use_icons=False):
		story = ""
		lanes = {1: "bottom", 2: "middle", 3: "top"}
		for laneid in lanes:
			story += "• {0[us]} {0[won_lost]} {1} lane vs {0[them]}\n".format(await self.get_lane_story(game['players'], laneid, is_radiant, use_icons), lanes[laneid])
		roamers = [self.get_pretty_hero(p, use_icons) for p in game['players'] if p.get('is_roaming')]
		if roamers:
			story += f"• {pretty_list(roamers)} roamed\n"
		return story

	async def tell_match_story(self, game, is_radiant, ctx, perspective=None):
		if not is_parsed(game):
			raise MatchNotParsedError(game["match_id"], "create a story")

		if not perspective:
			perspective = "The Radiant" if is_radiant else "The Dire"
			end_perspective = perspective
		else:
			end_perspective = f"{perspective} and their friends"

		story = (f"*Told from the perspective of {perspective}*\n"
				f"To see a more extensive story, try the [story tab](https://www.opendota.com/matches/{game['match_id']}/story) on opendota\n\n")

		story += await self.get_firstblood_story(game, is_radiant)

		story += await self.get_lane_stories(game, is_radiant)

		teamfights = await self.get_teamfight_stories(game, is_radiant)

		game_ending_state = "won" if (is_radiant == game['radiant_win']) else "lost"
		story_end = f"\n{end_perspective} {game_ending_state} the game at { get_pretty_duration(game['duration']) }"

		i = 0
		while i < len(teamfights) and (len(story) + len(teamfights[i]) + len(story_end)) < 2000:
			story += f"\n\n{teamfights[i]}"
			i += 1

		embed = disnake.Embed(description=story, color=self.embed_color)
		embed.title = f"Story of Match {game['match_id']}"
		embed.url = f"https://www.opendota.com/matches/{game['match_id']}/story"
		embed.set_footer(text=f"For more information, try {self.cmdpfx(ctx)}match {game['match_id']}")
		await ctx.send(embed=embed)


	# prints the stats for the given player's latest game
	async def player_match_stats(self, steamid, match_id, ctx):
		match = await get_match(match_id)

		# Finds the player in the game which has our matching steam32 id
		player = next((p for p in match['players'] if p['account_id'] == steamid), None)
		if player is None:
			await self.print_match_stats(ctx, match_id)
			return

		dotabase = self.bot.get_cog("Dotabase")
		hero_name = self.hero_info[player['hero_id']]['name']

		duration = get_pretty_duration(match['duration'], postfix=False)
		winstatus = "Won" if player["win"] != 0 else "Lost"
		game_mode = self.dota_game_strings.get(f"game_mode_{match.get('game_mode')}", "Unknown")
		lobby_type = self.dota_game_strings.get(f"lobby_type_{match.get('lobby_type')}", "Unknown") + " "
		if lobby_type == "Normal ":
			lobby_type = ""

		description = (f"{winstatus} a {lobby_type}**{game_mode}** match as {hero_name} in {duration}. "
					f"More info at [DotaBuff](https://www.dotabuff.com/matches/{match_id}), "
					f"[OpenDota](https://www.opendota.com/matches/{match_id}), or "
					f"[STRATZ](https://www.stratz.com/match/{match_id})")

		utc_timestamp = datetime.datetime.fromtimestamp(match['start_time'], tz=datetime.timezone.utc)
		embed = disnake.Embed(description=description, color=self.embed_color, timestamp=utc_timestamp)

		embed.set_author(name=player.get('personaname') or "Anonymous", icon_url=self.hero_info[player['hero_id']]['icon'], url="https://www.opendota.com/players/{}".format(steamid))

		damage_format = "KDA: **{kills}**/**{deaths}**/**{assists}**\n"
		if player.get("hero_damage") is not None:
			damage_format += "Hero Damage: {hero_damage:,}\n"
		if player.get("hero_healing") is not None:
			damage_format += "Hero Healing: {hero_healing:,}\n"
		if player.get("tower_damage") is not None:
			damage_format += "Tower Damage: {tower_damage:,}\n"
		embed.add_field(name="Damage", value=damage_format.format(**player))

		if not player.get("total_gold"):
			player["total_gold"] = 0
		embed.add_field(name="Economy", value=(
			"Net Worth: {total_gold:,}\n"
			"Last Hits: {last_hits:,}\n"
			"Denies: {denies}\n"
			"Level: {level}\n".format(**player)))

		match_image = disnake.File(await drawdota.create_match_image(match), "match.png")
		embed.set_image(url=f"attachment://{match_image.filename}")
		embed.set_footer(text=str(match_id))

		await ctx.send(embed=embed, file=match_image)

	@commands.command(aliases=["lastgame", "lm"])
	async def lastmatch(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets info about the player's last dota game

		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`"""
		await ctx.channel.trigger_typing()
		
		matchfilter = await MatchFilter.init(matchfilter, ctx)
		player = matchfilter.player

		match_id = await get_lastmatch_id(matchfilter)
		await self.player_match_stats(player.steam_id, match_id, ctx)

	@commands.command(aliases=["firstgame", "fm"])
	async def firstmatch(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets info about the player's first dota game

		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`"""
		await ctx.channel.trigger_typing()
		
		matchfilter = await MatchFilter.init(matchfilter, ctx)
		player = matchfilter.player

		match_id = await get_lastmatch_id(matchfilter, reverse=True)
		await self.player_match_stats(player.steam_id, match_id, ctx)

	async def print_match_stats(self, ctx, match_id):
		match = await get_match(match_id)
		duration = get_pretty_duration(match['duration'], postfix=False)
		game_mode = self.dota_game_strings.get(f"game_mode_{match.get('game_mode')}", "Unknown")
		lobby_type = self.dota_game_strings.get(f"lobby_type_{match.get('lobby_type')}", "Unknown") + " "
		if lobby_type == "Normal ":
			lobby_type = ""

		description = (f"This {lobby_type}**{game_mode}** match ended in {duration} \n"
					f"More info at [DotaBuff](https://www.dotabuff.com/matches/{match_id}), "
					f"[OpenDota](https://www.opendota.com/matches/{match_id}), or "
					f"[STRATZ](https://www.stratz.com/match/{match_id})")

		utc_timestamp = datetime.datetime.fromtimestamp(match['start_time'], tz=datetime.timezone.utc)
		embed = disnake.Embed(description=description, 
							timestamp=utc_timestamp, color=self.embed_color)
		embed.set_author(name="Match {}".format(match_id), url="https://www.opendota.com/matches/{}".format(match_id))

		embed.add_field(name="Game Mode", value=game_mode)
		embed.add_field(name="Lobby Type", value=game_mode)

		match_image = disnake.File(await drawdota.create_match_image(match), filename="matchimage.png")

		embed.set_image(url=f"attachment://{match_image.filename}")
		embed.set_footer(text=str(match_id))
		await ctx.send(embed=embed, file=match_image)


	@commands.command(aliases=["matchdetails"])
	async def match(self, ctx, match_id : int):
		"""Gets a summary of the dota match with the given id"""
		await ctx.channel.trigger_typing()
		await self.print_match_stats(ctx, match_id)


	@commands.command()
	async def matchstory(self, ctx, match_id : int, perspective=None):
		"""Tells the story of the match

		The story is based on the given perspective, or the player's perspective if they were in the match."""
		await ctx.channel.trigger_typing()

		steamid = None
		try:
			player = await DotaPlayer.from_author(ctx)
			steamid = player.steam_id
		except Exception as e:
			pass

		match = await get_match(match_id)

		if perspective is None:
			player_data = next((p for p in match['players'] if p['account_id'] == steamid), None)
			if steamid is not None and player_data is not None:
				is_radiant = player_data['isRadiant']
				perspective = "{2}({0}, {1})".format(self.get_pretty_hero(player_data), "Radiant" if is_radiant else "Dire", ctx.message.author.mention)
			else:
				is_radiant = True
		elif perspective.lower() == "radiant":
			is_radiant = True
			perspective = None
		elif perspective.lower() == "dire":
			is_radiant = False
			perspective = None
		else:
			raise UserError("Perspective must be either radiant or dire")
		

		await self.tell_match_story(match, is_radiant, ctx, perspective)

	@commands.command(aliases=["lastgamestory", "lmstory"])
	async def lastmatchstory(self, ctx, player : DotaPlayer = None):
		"""Tells the story of the player's last match

		Input must be either a discord user, a steam32 id, or a steam64 id"""
		await ctx.channel.trigger_typing()
		if not player:
			player = await DotaPlayer.from_author(ctx)

		perspective = player.mention
		try:
			match_id = (await opendota_query(f"/players/{player.steam_id}/matches?limit=1"))[0]['match_id']
			game = await get_match(match_id)
		except UserError:
			await ctx.send("I can't find the last game this player played")
			return
		if player is None:
			player = ctx.message.author.mention

		player_data = next((p for p in game['players'] if p['account_id'] == player.steam_id), None)
		perspective += "({0}, {1})".format(self.get_pretty_hero(player_data), "Radiant" if player_data['isRadiant'] else "Dire")

		await self.tell_match_story(game, player_data['isRadiant'], ctx, perspective)

	@commands.command(aliases=["recentmatches", "recent"])
	async def matches(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets a list of your matches

		The date/time is localized based off of the server that the game was played on, which means it may not match your timezone.

		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`

		Note that you can have this show up to 100 matches, but will by default only show 10, unless a timespan is given

		**Example:**
		`{cmdpfx}matches @PlayerPerson mid witch doctor ranked`
		`{cmdpfx}matches natures prophet`
		`{cmdpfx}matches @PlayerPerson riki`"""
		await ctx.channel.trigger_typing()

		matchfilter = await MatchFilter.init(matchfilter, ctx)

		steam32 = matchfilter.player.steam_id

		matchfilter.set_arg("limit", 10, False)
		matchfilter.set_arg("significant", 0, False)

		limit_max = 100
		if matchfilter.get_arg("limit") > limit_max or matchfilter.has_value("date"):
			matchfilter.set_arg("limit", limit_max, True)

		if matchfilter.get_arg("limit") < 1:
			raise UserError("Limit of matches can't be less than 1")

		hero = matchfilter.hero

		matchfilter.add_projections([ "kills", "deaths", "assists", "hero_id", "version", "game_mode", "lobby_type", "region", "duration", "start_time" ])

		matches = await opendota_query_filter(matchfilter)
		if not matches:
			raise UserError("I can't find any matches that match that filter")

		matches = sorted(matches, key=lambda m: m.get("start_time"), reverse=True)


		embed = disnake.Embed()

		embed.title = "Recent Matches"
		embed.url = f"https://www.opendota.com/players/{steam32}/matches"
		if hero:
			embed.title += f" as {hero.localized_name}"
			embed.url += f"?hero_id={hero.id}"
			if hero.color:
				embed.color = disnake.Color(int(hero.color[1:], 16))

		matches_image = await drawdota.draw_matches_table(matches, self.dota_game_strings)
		matches_image = disnake.File(matches_image, "matches.png")
		embed.set_image(url=f"attachment://{matches_image.filename}")
		embed.set_footer(text=f"Try {self.cmdpfx(ctx)}matchids to get copy-pastable match ids")

		await ctx.send(embed=embed, file=matches_image)

	@commands.command()
	async def matchids(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets a list of matchids that match the given filter

		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`

		**Example:**
		`{cmdpfx}matchids @PlayerPerson mid witch doctor ranked`
		`{cmdpfx}matchids natures prophet`
		`{cmdpfx}matchids @PlayerPerson riki`"""
		await ctx.channel.trigger_typing()

		matchfilter = await MatchFilter.init(matchfilter, ctx)

		steam32 = matchfilter.player.steam_id

		matchfilter.set_arg("limit", 10, False)
		matchfilter.set_arg("significant", 0, False)

		limit_max = 100
		if matchfilter.get_arg("limit") > limit_max or matchfilter.has_value("date"):
			matchfilter.set_arg("limit", limit_max, True)

		if matchfilter.get_arg("limit") < 1:
			raise UserError("Limit of matches can't be less than 1")

		matchfilter.add_projections([ "kills", "deaths", "assists", "hero_id", "version", "game_mode", "lobby_type", "region", "duration", "start_time" ])

		matches = await opendota_query_filter(matchfilter)
		if not matches:
			raise UserError("I can't find any matches that match that filter")

		matches = sorted(matches, key=lambda m: m.get("start_time"), reverse=True)


		embed = disnake.Embed()

		embed.title = "Recent Matches"
		embed.url = f"https://www.opendota.com/players/{steam32}/matches"

		embed.description = "```\n"
		embed.description += "\n".join(list(map(lambda m: str(m["match_id"]), matches)))
		embed.description += "\n```"

		embed.set_footer(text=f"Try {self.cmdpfx(ctx)}matches to get more details about these matches")

		await ctx.send(embed=embed)

	@commands.command()
	async def meta(self, ctx, *, count : int = 10): 
		"""prints the top meta heroes from https://opendota.com/heroes"""
		await ctx.channel.trigger_typing()
		if count > 119:
			raise UserError("Limit of matches can't be more than the number of heroes")
		if count < 1: 
			raise UserError("Limit of matches can't be less than 1")
		await self.print_meta(ctx, count)

	@commands.command(aliases=["whois"])
	async def profile(self, ctx, player : DotaPlayer = None):
		"""Displays information about the player's dota profile

		The argument for this command can be either a steam32 id, a steam64 id, or an @mention of a discord user who has a steamid set"""
		if not player:
			player = await DotaPlayer.from_author(ctx)
		steam32 = player.steam_id

		await ctx.channel.trigger_typing()

		playerinfo = await opendota_query(f"/players/{steam32}")
		matches = await opendota_query(f"/players/{steam32}/matches")
		matches = list(filter(lambda m: m.get('player_slot') is not None, matches))

		rank_string = self.get_player_rank(playerinfo)

		gamesplayed = len(matches)
		if gamesplayed > 0:
			winrate = "{:.2%}".format(len(list(filter(lambda m: m.get('radiant_win', False) == ((m.get('player_slot', 0) or 0) < 128), matches))) / gamesplayed)
		else:
			winrate = "0%"

		heroes = {}
		for match in matches:
			heroes[match['hero_id']] = heroes.get(match['hero_id'], 0) + 1
		heroes = sorted(heroes.items(), key=lambda x: x[1], reverse=True)
		favs = ""
		for i in range(0,3):
			if i < len(heroes):
				favs += self.hero_info[heroes[i][0]]['emoji']

		# Recent means 2 months / 60 days 
		timecutoff = time.time() - (86400 * 60)

		heroes = {}
		for match in matches:
			if match['start_time'] > timecutoff:
				heroes[match['hero_id']] = heroes.get(match['hero_id'], 0) + 1
		heroes = sorted(heroes.items(), key=lambda x: x[1], reverse=True)
		recent_favs = ""
		for i in range(0,3):
			if i < len(heroes):
				recent_favs += self.hero_info[heroes[i][0]]['emoji']

		recent_count = 0
		activity_delta = []
		activity_count = []
		count = 1
		for i in range(0, len(matches) - 1):
			delta = matches[i]["start_time"] - (matches[i + 1]["start_time"] + matches[i]["duration"])
			if delta < (60 * 60 * 2): # If these are part of the same group
				count += 1
				continue
			else:
				activity_count.append(count)
				activity_delta.append(delta)
				count = 1
				if matches[i]["start_time"] > timecutoff:
					recent_count += 1

		if not activity_delta:
			activity_delta = [ 0 ]
			activity_count = [ 0 ]

		overall_time_played = 0
		for match in matches:
			overall_time_played += match["duration"]

		overall_activity_delta = get_pretty_time((int(statistics.mean(activity_delta)) // 60) * 60)
		if recent_count:
			recent_activity_delta = get_pretty_time((int(statistics.mean(activity_delta[:recent_count])) // 60) * 60)
		else:
			recent_activity_delta = None
		# overall_activity_count = int(statistics.mean(activity_count))
		# recent_activity_count = int(statistics.mean(activity_count[:recent_count]))

		plus_text = ""
		if playerinfo["profile"].get("plus"):
			plus_text = f"\n{self.get_emoji('dota_plus')} has Dota Plus"

		embed = disnake.Embed(color=self.embed_color)

		embed.set_author(
			name=playerinfo["profile"]["personaname"] or "Anonymous", 
			icon_url=playerinfo["profile"]["avatar"] or default_steam_icon, 
			url=playerinfo["profile"]["profileurl"] or f"https://www.opendota.com/players/{steam32}")

		embed.add_field(name="General", value=(
			f"Winrate: **{winrate}**\n"
			f"Games Played: **{gamesplayed}**\n"
			f"Total Hours In Game: **{overall_time_played // 3600:,}**\n"
			f"{rank_string}"
			f"{plus_text}"))

		embed.add_field(name="Profiles", value=(
			f"[Steam]({playerinfo['profile']['profileurl']})\n"
			f"[OpenDota](https://www.opendota.com/players/{steam32})\n"
			f"[DotaBuff](https://www.dotabuff.com/players/{steam32})\n"
			f"[STRATZ](https://www.stratz.com/player/{steam32})"))

		embed.add_field(name="Heroes", value=(
			f"[Recent Favs](https://www.opendota.com/players/{steam32}/heroes?date=60) {recent_favs}\n"
			f"[Overall Favs](https://www.opendota.com/players/{steam32}/heroes) {favs}\n"))

		embed.add_field(name="Activity", value=(
			"*Average time between groups of games*\n"
			f"**Recent**: {recent_activity_delta}\n"
			f"**Overall**: {overall_activity_delta}\n"), inline=False)

		
		if player.is_author:
			player_mention = ""
		else:
			player_mention = player.steam_id

		rank_icon = await drawdota.dota_rank_icon(playerinfo.get("rank_tier"), playerinfo.get("leaderboard_rank"))
		rank_icon = disnake.File(rank_icon, "rank.png")
		embed.set_thumbnail(url=f"attachment://{rank_icon.filename}")

		embed.set_footer(text=f"Steam ID: {steam32}")

		await ctx.send(embed=embed, file=rank_icon)

	@commands.command(aliases=["chatstats"])
	async def twenty(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets stats from the player's last 20 parsed games

		Note that this only cares about **parsed** games, and unparsed games will be ignored. If the player has less than 20 parsed matches, we'll use all the parsed matches available

		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`"""
		matchfilter = await MatchFilter.init(matchfilter, ctx)
		matchfilter.set_arg("limit", 20, True)
		matchfilter.set_arg("_parsed", True)

		await self.do_playerstats(ctx, matchfilter, do_downloaded=True)

	@commands.command(aliases=["pstats", "herostats"])
	async def playerstats(self, ctx, *, matchfilter : MatchFilter = None):
		"""Gets player match statistics

		By default this will target all the matches a player has played.

		**Note:** If you're wondering why some data is now missing, check out `{cmdpfx}twenty`. I've revamped this command to work for all matches, and `{cmdpfx}twenty` is the old version of what this command used to be.
		
		To see how to filter for specific matches, try `{cmdpfx}docs matchfilter`"""
		matchfilter = await MatchFilter.init(matchfilter, ctx)

		await self.do_playerstats(ctx, matchfilter)

	# the main internal logic for the playerstats and twenty commands
	async def do_playerstats(self, ctx, matchfilter, do_downloaded=False):
		matchfilter.add_projections([ "kills", "deaths", "assists", "party_size", "version", "hero_id", "lane_role", "is_roaming", "lobby_type", "start_time", "duration" ])
		steam32 = matchfilter.player.steam_id

		# 
		# STEP 1: download all match data
		# 
		with ctx.channel.typing():
			await thinker.think(ctx.message)
			playerinfo = await opendota_query(f"/players/{steam32}")
			matches_info = await opendota_query_filter(matchfilter)
			matches_info = sorted(matches_info, key=lambda m: m["start_time"])
			player_matches = []

			if do_downloaded:
				matches = []
				i = 0
				while i < len(matches_info) and len(matches) < 20:
					if matches_info[i].get('version', None) is not None:
						match = await get_match(matches_info[i]['match_id'])
						player_match = next((p for p in match['players'] if p['account_id'] == steam32), None)
						if player_match is not None:
							player_matches.append(player_match)
							matches.append(match)
					i += 1
			else:
				player_matches = matches_info


		await thinker.stop_thinking(ctx.message)
		if len(player_matches) == 0:
			if do_downloaded:
				await ctx.send("Not enough parsed matches!")
			else:
				await ctx.send("Not enough matches found!")
			return

		# 
		# STEP 2: initialize discord embed, depending on what we filtered for
		# 
		embed = disnake.Embed(color=self.embed_color)
		embed_attachment = None

		if do_downloaded:
			embed.description = f"*The following are averages and percentages based on the last {len(player_matches)} parsed matches*"
		else:
			embed.description = ""
		embed.set_footer(text=f"To see the filtering options for this command, try \"{self.cmdpfx(ctx)}docs matchfilter\"")

		matches_url = f"https://www.opendota.com/players/{steam32}/matches?{matchfilter.to_query_args(for_web_url=True)}"
		author_name = playerinfo["profile"]["personaname"] or "Anonymous"
		author_icon_url = playerinfo["profile"]["avatar"] or default_steam_icon

		# if this is stats for playing as a specific hero
		if matchfilter.has_value("hero_id"):
			hero = self.lookup_hero(matchfilter.get_arg("hero_id"))
			author_icon_url = self.hero_info[hero.id]["icon"]
			embed.set_thumbnail(url=self.hero_info[hero.id]['portrait'])
			embed.color = disnake.Color(int(hero.color[1:], 16))

		# if this is stats for playing with someone
		if matchfilter.has_value("included_account_id"):
			# make friends image
			avatar1 = playerinfo['profile']['avatarfull'] or default_steam_icon
			player2_id = matchfilter.get_arg("included_account_id")
			player2_info = await opendota_query(f"/players/{player2_id}")
			avatar2 = player2_info['profile']['avatarfull'] or default_steam_icon
			image = disnake.File(await drawdota.combine_image_halves(avatar1, avatar2), "profile.png")
			embed.set_thumbnail(url=f"attachment://{image.filename}")
			embed_attachment = image
			author_name += f" + {player2_info['profile']['personaname'] or 'Anonymous'}"

		# also add the dates of first and last match to description
		first_match = player_matches[0]
		last_match = player_matches[-1]
		def get_time_diff(match):
			timediff = time.time() - match["start_time"]
			timediff -= timediff % 60 # only show up to minutes level of detail
			if timediff > (29 * 60 * 60 * 24): # if was over a month ago
				timediff -= (timediff % (60 * 60 * 24)) # only show up to days level of detail
			if timediff > (3 * 60 * 60 * 24): # if was over a couple day ago
				timediff -= (timediff % (60 * 60)) # only show up to hours level of detail
			return get_pretty_time(timediff)
		embed.description += f"\n[First Match](https://www.opendota.com/matches/{first_match['match_id']}): {get_time_diff(first_match)} ago"
		embed.description += f"\n[Last Match](https://www.opendota.com/matches/{last_match['match_id']}): {get_time_diff(last_match)} ago"

		embed.set_author(
			name=author_name, 
			icon_url=author_icon_url, 
			url=matches_url)

		# 
		# STEP 3: define all stats together
		# 
		def avg(*args, **kwargs):
			return _match_avg(player_matches, *args, **kwargs)
		def percent(*args, **kwargs):
			return _match_percent(player_matches, *args, **kwargs)

		# compute favorites
		heroes = {}
		for match in player_matches:
			heroes[match['hero_id']] = heroes.get(match['hero_id'], 0) + 1
		heroes = sorted(heroes.items(), key=lambda x: x[1], reverse=True)
		favorite_heroes = "".join(map(lambda h: self.hero_info[h[0]]['emoji'], heroes[0:3]))
		zeropercent = "0%"

		# laning postfix if needed
		laning_postfix = ""
		parsed_count = len(list(filter(lambda m: m.get("version") is not None, player_matches)))
		if parsed_count != len(player_matches) and not do_downloaded:
			laning_postfix = f" ({parsed_count} parsed matches)"

		class CoolStat():
			def __init__(self, caption, value, filter_key=None, ignore_value=None, separator=": ", bold=True):
				self.caption = caption
				self.value = value
				self.filter_key = filter_key
				self.ignore_value = ignore_value
				self.separator = separator
				self.bold = bold

			def should_show(self):
				return not(self.value == self.ignore_value or matchfilter.has_value(self.filter_key))

			def render(self):
				if not self.caption:
					return str(self.value)
				value = self.value
				if self.bold:
					value = f"**{value}**"
				return f"{self.caption}{self.separator}{value}"

		stat_sections = [
			{
				"caption": "General",
				"stats": [
					CoolStat(f"[Matches]({matches_url})", len(player_matches)),
					CoolStat("Winrate", percent(lambda p: p.get('radiant_win') == (p.get('player_slot') < 128)), filter_key="win"),
					CoolStat("KDA", f"{avg('kills')}/{avg('deaths')}/{avg('assists')}"),
					CoolStat("Duration", format_duration_simple(avg('duration') or 0)),
					CoolStat("In a Party", percent(lambda p: p.get('party_size') > 1, needs_key='party_size', round_place="floor")),
					CoolStat("Ranked", percent(lambda p: p['lobby_type'] == 7), filter_key="lobby_type")
				]
			},
			{
				"caption": "Heroes",
				"filter_key": "hero_id",
				"stats": [
					CoolStat(self.get_emoji('attr_strength'), percent(lambda p: self.hero_info.get(p['hero_id'], {}).get('attr') == 'strength'), separator=" "),
					CoolStat(self.get_emoji('attr_agility'), percent(lambda p: self.hero_info.get(p['hero_id'], {}).get('attr') == 'agility'), separator=" "),
					CoolStat(self.get_emoji('attr_intelligence'), percent(lambda p: self.hero_info.get(p['hero_id'], {}).get('attr') == 'intelligence'), separator=" "),
					CoolStat("Randomed", percent('randomed'), ignore_value=zeropercent),
					CoolStat("__Favorites__", f"\n{favorite_heroes}")
				]
			},
			{
				"caption": f"Laning{laning_postfix}",
				"filter_key": "lane_role",
				"stats": [
					CoolStat("Safe Lane", percent(lambda p: p.get('lane_role') == 1 and not p.get('is_roaming'), needs_key="lane_role"), ignore_value=zeropercent),
					CoolStat("Mid Lane", percent(lambda p: p.get('lane_role') == 2 and not p.get('is_roaming'), needs_key="lane_role"), ignore_value=zeropercent),
					CoolStat("Off Lane", percent(lambda p: p.get('lane_role') == 3 and not p.get('is_roaming'), needs_key="lane_role"), ignore_value=zeropercent),
					CoolStat("Jungle", percent(lambda p: p.get('lane_role') == 4 and not p.get('is_roaming'), needs_key="lane_role"), ignore_value=zeropercent),
					CoolStat("Roaming", percent(lambda p: p.get('is_roaming'), needs_key="is_roaming"), ignore_value=zeropercent),
				]
			}
		]

		if do_downloaded:# if we've downloaded all of these matches, compute all the chat history stuff, and add the downloaded stats
			chat_wheel_counts = {}
			chat_wheel_total = 0
			longest_message_heading = "Longest Chat Message"
			message_count = 0
			longest_message = None
			longest_message_match_id = None
			for match in matches:
				player = next((p for p in match['players'] if p['account_id'] == steam32), None)
				match_chat = match.get('chat', None)
				if match_chat:
					for message in match_chat:
						if message.get('player_slot', -1) == player['player_slot']:
							if message["type"] == "chat":
								message_count += 1
								if longest_message is None or len(longest_message) <= len(message['key']):
									longest_message = message['key']
									longest_message_match_id = match['match_id']
							elif message["type"] == "chatwheel":
								msg_id = int(message['key'])
								if msg_id >= 1000:
									continue # skip hero chat wheels
								chat_wheel_counts[msg_id] = chat_wheel_counts.get(msg_id, 0) + 1
								chat_wheel_total += 1

			message_count = int(round(message_count / len(matches)))
			if longest_message is not None:
				longest_message = f"\"{longest_message}\""
				longest_message_heading = f"[{longest_message_heading}](https://www.opendota.com/matches/{longest_message_match_id}/chat)"

			chat_wheel_text = "*No chat wheel usage found*"
			if chat_wheel_counts != {}:
				lines = []
				chat_wheel_counts = sorted(chat_wheel_counts.items(), key=lambda m: m[1], reverse=True)
				for i in range(0, min(3, len(chat_wheel_counts))):
					msg_id, count = chat_wheel_counts[i]
					message = self.chat_wheel_info.get(msg_id, { "message": "Unknown" })
					icon = self.get_emoji("chat_wheel_sound" if message.get('is_sound') else "chat_wheel_text")
					lines.append(f"{icon} {message['message']}")
				chat_wheel_text = "\n".join(lines)
			def wards_placed(p):
				obs = 0 if p.get('obs_placed') is None else p.get('obs_placed')
				sents = 0 if p.get('sen_placed') is None else p.get('sen_placed')
				return obs + sents

			# these are the downloaded_only sections
			stat_sections.extend([{
				"caption": "Economy",
				"stats": [
					CoolStat("GPM", avg('gold_per_min')),
					CoolStat("XPM", avg('xp_per_min')),
					CoolStat("Last Hits/min", avg(lambda p: p['last_hits'] / (1 + (p['duration'] / 60)), 2)),
					CoolStat("Neutral Creeps", avg(lambda p: 100 * p.get('neutral_kills', 0) / (1 + p['last_hits'])))
				]
			},
			{
				"caption": "Other",
				"stats": [
					CoolStat("APM", avg('actions_per_min')),
					CoolStat("Pings", avg('pings')),
					CoolStat("Wards Placed", avg(lambda p: wards_placed(p)))
				]
			},
			{
				"caption": "Chat Wheel",
				"stats": [ 
					CoolStat(None, chat_wheel_text)
				]
			},
			{
				"caption": "All Chat",
				"inline": False,
				"stats": [
					CoolStat("Messages per Game", message_count),
					CoolStat(longest_message_heading, longest_message, bold=False)
				]
			}])

		# 
		# STEP 4: transform these all into embed fields
		# 
		for category in stat_sections:
			if category.get("filter_key") and matchfilter.has_value(category.get("filter_key")):
				continue # skip this category if its already filtered out by the matchfilter
			value = "\n".join(map(lambda s: s.render(), filter(lambda s: s.should_show(), category.get("stats"))))
			if value == "":
				continue # skip if theres no values to show
			embed.add_field(name=category.get("caption"), value=value, inline=category.get("inline", True))

		if embed_attachment:
			await ctx.send(embed=embed, file=image)
		else:
			await ctx.send(embed=embed)

	@commands.command(aliases=["dota_gif"])
	async def dotagif(self, ctx, match_id : int, start, end, ms_per_second : int = 100):
		"""Creates a gif of a specific part of a dota match

		The part of the match that you specify must be less than 10 minutes long

		`ms_per_second` is how many miliseconds between frames of the gif (each frame is 1 dota second)

		**Example:**
		`{cmdpfx}dotagif 3370877768 28:37 30:30`"""
		await ctx.channel.trigger_typing()

		match = await get_match(match_id)
		if not is_parsed(match):
			raise MatchNotParsedError(match_id, "get laning info")

		stratz_match = await get_stratz_match(match_id)
		if not is_stratz_parsed(stratz_match):
			raise StratzMatchNotParsedError(match_id)


		start = int(get_time(start))
		end = int(get_time(end))

		if end - start > 600:
			raise UserError("The length of this clip must be less than 10 minutes")

		if ms_per_second < 1 or ms_per_second > 655350:
			raise UserError("That is outside the bounds of the `ms_per_second` value")

		lastframe = match["duration"] - 1
		if start > lastframe and end > lastframe:
			raise UserError("The game didn't last that long")

		# "https://stratz.com/en-us/match/{match_id}/playback?pb_time={seconds}"

		async with ctx.channel.typing():
			await thinker.think(ctx.message)
			try:
				image = disnake.File(await self.create_dota_gif(match, stratz_match, start, end, ms_per_second), "map.gif")
				await ctx.send(file=image)
			finally:
				await thinker.stop_thinking(ctx.message)

	@commands.command(aliases=["lanes"])
	async def laning(self, ctx, match_id : int = None):
		"""Creates gif of the laning stage with a caption

		If no match id is given and the user has a steam account connected, uses the player's most recently played match"""
		await ctx.channel.trigger_typing()
		try:
			player = await DotaPlayer.from_author(ctx)
			steamid = player.steam_id
		except CustomBadArgument:
			steamid = None
			pass
		if match_id is None:
			if steamid is None:
				raise SteamNotLinkedError()
			matchfilter = await MatchFilter.init(None, ctx)
			match_id = await get_lastmatch_id(matchfilter)
		

		match = await get_match(match_id)
		if not is_parsed(match):
			raise MatchNotParsedError(match_id, "get laning info")

		stratz_match = await get_stratz_match(match_id)
		if not is_stratz_parsed(stratz_match):
			raise StratzMatchNotParsedError(match_id)

		player_data = None
		if steamid:
			player_data = next((p for p in match['players'] if p['account_id'] == steamid), None)
		perspective = player_data.get("isRadiant") if player_data else True

		embed = disnake.Embed(description=await self.get_lane_stories(match, perspective, True))

		embed.title = f"Laning"
		embed.url = f"https://stratz.com/en-us/match/{match_id}/playback"


		async with ctx.channel.typing():
			await thinker.think(ctx.message)
			try:
				image = disnake.File(await self.create_dota_gif(match, stratz_match, -89, 600, 100), "map.gif")
				embed.set_image(url=f"attachment://{image.filename}")
				await ctx.send(embed=embed, file=image)
			finally:
				await thinker.stop_thinking(ctx.message)


	@commands.command(aliases=["analyze", "studymatch"])
	async def parse(self, ctx, match_id : int = None):
		"""Requests that OpenDota parses a match

		The input should be the match_id of the match

		Note that matches from more than a couple days ago may not be able to be parsed because replay files are not saved that long

		Not giving a matchid will make mangobyte attempt to use your last played match"""
		if match_id is None:		
			matchfilter = await MatchFilter.init(None, ctx)
			match_id = await get_lastmatch_id(matchfilter)

		await ctx.message.add_reaction("⏳")
		await ctx.send("⏳ Requesting a parse...", delete_after=5)

		try:
			data = await httpgetter.post(f"https://api.opendota.com/api/request/{match_id}", errors=opendota_html_errors)
		except HttpError as e:
			await ctx.message.remove_reaction("⏳", self.bot.user)
			if e.code == 400:
				await ctx.send("❌ Looks like that's not a valid match id")
				return
			raise

		if data.get("status") == "failed" or data.get("err") is not None:
			await ctx.message.remove_reaction("⏳", self.bot.user)
			await ctx.send(f"❌ There was an error requesting the parse for match {match_id}")
			return

		jobId = data["job"]["jobId"]
		await asyncio.sleep(3)
		seconds_per_check = 20
		seconds_till_timeout = 120

		while seconds_till_timeout > 0:
			data = await opendota_query(f"/request/{jobId}", False)

			if data is not None:
				await asyncio.sleep(seconds_per_check)
				seconds_till_timeout -= seconds_per_check
			else:
				await ctx.message.remove_reaction("⏳", self.bot.user)
				await ctx.message.add_reaction("✅")
				await ctx.send(f"✅ Parsing of match {match_id} has completed!", delete_after=10)
				return

		# if we get to here, timeout
		await ctx.message.remove_reaction("⏳", self.bot.user)
		await ctx.message.add_reaction("❌")
		await ctx.send(f"❌ Parsing of match {match_id} timed out. Try again later or on the opendota site.", delete_after=10)


	@commands.command(aliases=["profiles"])
	async def whoishere(self, ctx, *, mentions_or_rank = None):
		"""Shows what discord users are which steam users

		This command will take the users that are currently in the channel mangobyte is in, and create an embed that shows who they are in steam. If you are in a voice channel, it will use the channel that you are in

		You can also mention the users you want to show and it will show those ones too

		If you use the word `rank` somewhere in the command, it will also show the ranks of the players"""
		if ctx.message.guild is None:
			raise UserError("You have to use that command in a server")

		voice_channel = None
		if ctx.author.voice and ctx.author.voice.channel:
			voice_channel = ctx.author.voice.channel
		else:
			audio = self.bot.get_cog("Audio")
			audioplayer = await audio.audioplayer(ctx, False)
			if audioplayer is None or audioplayer.voice_channel is None:
				if len(ctx.message.mentions) == 0:
					raise UserError("One of us needs to be in a voice channel for that to work")
			else:
				voice_channel = audioplayer.voice_channel

		show_ranks = "rank" in (mentions_or_rank if mentions_or_rank else "")

		members = []
		if voice_channel:
			members.extend(voice_channel.members)
		if ctx.message.mentions:
			members.extend(ctx.message.mentions)

		mentions = []
		links = []
		ranks = []

		for member in members:
			if voice_channel:
				if member.id == voice_channel.guild.me.id:
					continue
			mentions.append(member.mention)
			userinfo = botdata.userinfo(member.id)
			if userinfo.steam is None:
				links.append("Unknown")
				ranks.append("Unknown")
			else:
				player_info = await opendota_query(f"/players/{userinfo.steam}")
				links.append(f"[{player_info['profile']['personaname']}](https://www.opendota.com/players/{userinfo.steam})")
				ranks.append(self.get_player_rank(player_info))


		if len(mentions) == 0:
			raise UserError("There isn't anyone in my voice channel 😢")
		#raise UserError("This command is broken right now but my developer is working on fixing it! For now you can mention people manually in the command and it should work.")

		embed = disnake.Embed()
		embed.add_field(name="Discord", value="\n".join(mentions))
		embed.add_field(name="Steam", value="\n".join(links))
		if show_ranks:
			embed.add_field(name="Rank", value="\n".join(ranks))

		await ctx.send(embed=embed)

	@commands.command()
	async def opendota(self, ctx, *, query):
		"""Queries the opendota api

		You can use this to get a json file with details about players or matches etc.
		Examples:
		`{cmdpfx}opendota /players/{steamid}`
		`{cmdpfx}opendota /matches/{match_id}`

		Note that this is just a little tool showcasing how you can use the api. You can also put urls like these in your browser to get the same results, which I'd recommend if you're doing this a lot.

		For more options and a better explanation, check out their [documentation](https://docs.opendota.com)"""
		query = query.replace("/", " ")
		query = query.strip()
		query = "/" + "/".join(query.split(" "))
		query = re.sub("[^/0-9a-zA-Z?=&_]", "", query)

		with ctx.channel.typing():
			data = await opendota_query(query)

		tempdir = settings.resource("temp")
		if not os.path.exists(tempdir):
			os.makedirs(tempdir)
		filename = re.search("/([/0-9a-zA-Z]+)", query).group(1).replace("/", "_")
		filename = tempdir + f"/{filename}.json"
		write_json(filename, data)
		await ctx.send(file=disnake.File(filename))
		os.remove(filename)

	@commands.command()
	async def rolesgraph(self, ctx, player : DotaPlayer = None):
		"""Gets a graph displaying the player's hero roles

		The graph is based on the player's last 30 games
		"""
		if not player:
			player = await DotaPlayer.from_author(ctx)

		playerinfo = await opendota_query(f"/players/{player.steam_id}")
		matches = await opendota_query(f"/players/{player.steam_id}/matches?limit=30")

		if len(matches) == 0:
			raise UserError("You haven't played any matches recently")

		hero_ids = []
		for match in matches:
			hero_ids.append(match["hero_id"])
		roles = [ "Escape", "Nuker", "Support", "Pusher", "Disabler", "Jungler", "Carry", "Durable", "Initiator" ]
		role_scores = dict.fromkeys(roles, 0)

		dotabase = self.bot.get_cog("Dotabase")
		for heroid in hero_ids:
			hero_info = self.hero_info[heroid]
			for role, value in hero_info["roles"].items():
				role_scores[role] += value

		role_scores = [role_scores[role] for role in roles]

		# weight it against the biases in the system
		role_totals = dict.fromkeys(roles, 0)
		for hero_info in self.hero_info.values():
			for role, value in hero_info["roles"].items():
				role_totals[role] += value
		role_totals = role_totals.values()
		role_totals_avg = sum(role_totals) / len(role_totals)
		role_totals_modifiers = list(map(lambda x: role_totals_avg / x, role_totals))
		for i in range(len(roles)):
			role_scores[i] *= role_totals_modifiers[i]

		# normalize so its a percentage based on the highest one
		divisor = max(role_scores)
		role_scores = list(map(lambda x: x / divisor, role_scores))

		embed = disnake.Embed()
		embed.set_author(
			name=playerinfo["profile"]["personaname"] or "Anonymous", 
			icon_url=playerinfo["profile"]["avatar"] or default_steam_icon, 
			url=playerinfo["profile"]["profileurl"] or f"https://www.opendota.com/players/{player.steam_id}")

		image = disnake.File(drawdota.draw_polygraph(role_scores, roles), "rolesgraph.png")
		embed.set_image(url=f"attachment://{image.filename}")
		await ctx.send(embed=embed, file=image)

	@commands.command(aliases=["abilitybuild", "skillbuilds", "matchbuilds"])
	async def skillbuild(self, ctx, match_id : int):
		"""Gets the ability upgrades for a match

		Shows all the ability upgrade orders for all heroes in the match"""
		match = await get_match(match_id)

		embed = disnake.Embed()

		embed.title = f"Match {match_id}"
		embed.url = f"https://opendota.com/matches/{match_id}"

		async with ctx.channel.typing():
			image = disnake.File(await drawdota.draw_match_ability_upgrades(match), "upgrades.png")
			embed.set_image(url=f"attachment://{image.filename}")
			await ctx.send(embed=embed, file=image)

	@commands.command(aliases=["graph", "dotagraph"])
	async def matchgraph(self, ctx, *, options = ""):
		"""Creates a graph for a dota match

		Give this match a match_id or it will try to use your last played game

		different types of graphs:
		teamdiff: creates a graph of the networth/xp differences between the teams
		playergold: creates a graph of the networths of the players throughout the match
		(ill probably add more in the futre but thats it for now)
		"""
		graphtypes = {
			"teamdiff": {
				"pattern": "(team)? ?(diff|networth)",
				"name": "Team Gold/Experience Difference"
			},
			"playergold": {
				"pattern": "(players? ?(gold)?)",
				"name": "Player Gold"
			}
		}

		graphtype = "teamdiff"

		for key in graphtypes:
			pattern = graphtypes[key]["pattern"]
			if re.match(pattern, options):
				options = re.sub(pattern, "", options)
				graphtype = key
				break

		options = options.strip()

		if options.isnumeric():
			match_id = int(options)
		elif options == "":
			try:
				player = await DotaPlayer.from_author(ctx)
				steamid = player.steam_id
			except CustomBadArgument:
				steamid = None
				raise SteamNotLinkedError()
			matchfilter = await MatchFilter.init(None, ctx)
			match_id = await get_lastmatch_id(matchfilter)
		else:
			raise UserError(f"I'm not sure what \"{options}\" means")

		match = await get_match(match_id)

		if not is_parsed(match):
			raise MatchNotParsedError(match["match_id"], "create a graph")

		embed = disnake.Embed()

		embed.title = f"Match {match_id}"
		embed.url = f"https://opendota.com/matches/{match_id}"
		embed.set_footer(text=f"This is a rough draft, im planning on making this much better soon")

		embed.description = graphtypes[graphtype]["name"]

		if graphtype == "teamdiff":
			lines = [ match["radiant_gold_adv"], match["radiant_xp_adv"] ]
			colors = [ "#FFFF00", "#ADD8E6" ]
			labels = [ "Gold", "Experience" ]
		elif graphtype == "playergold":
			playercolors = {
				"0": "#3375FF",
				"1": "#66FFBF",
				"2": "#BF00BF",
				"3": "#F3F00B",
				"4": "#FF6B00",
				"128": "#FE86C2",
				"129": "#A1B447",
				"130": "#65D9F7",
				"131": "#008321",
				"132": "#A46900"
			}
			lines = []
			colors = []
			labels = []
			for player in match["players"]:
				colors.append(playercolors[str(player["player_slot"])] if str(player["player_slot"]) else "#FF0000")
				lines.append(player["gold_t"])
				labels.append(self.hero_info[player["hero_id"]]["name"] if player["hero_id"] in self.hero_info else "Unknown")
		else:
			raise UserError("oops, look like thats not implemented yet")

		async with ctx.channel.typing():
			image = disnake.File(drawgraph.drawgraph(lines, colors, labels), "graph.png")
			embed.set_image(url=f"attachment://{image.filename}")
			await ctx.send(embed=embed, file=image)



	# @commands.command(aliases=["wrapped"])
	async def dotawrapped(self, ctx, player : DotaPlayer = None):
		"""Gets the "dota wrapped" summary for the player

		This is from the site https://gameishard.gg/dotawrapped/

		Yes, I got permission from the guy who made this to include this in mangobyte"""
		if not player:
			player = await DotaPlayer.from_author(ctx)

		wrapped_url = f"https://gameishard.gg/dotawrapped/?id={player.steam_id}"
		wrapped_image_url = f"https://gameishard.gg/dotawrapped/assets/images/players/{player.steam_id}.png"

		await thinker.think(ctx.message)
		await httpgetter.get(wrapped_url, return_type="text")
		await thinker.stop_thinking(ctx.message)

		embed = disnake.Embed()

		embed.title = f"Dota 2 Wrapped"
		embed.url = wrapped_url
		embed.set_image(url=wrapped_image_url)

		await ctx.send(embed=embed) class Pokemon(MangoCog):
	"""Pokemon related commands
	
	A few commands using the [pokeapi](https://pokeapi.co/) which provides information about pokemon

	Note that the above API does not include information about pokemon Sun and Moon, so I can't look up those pokemon for you"""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)

	def poke_type(self, type_name):
		if type_name == "shadow" or type_name == "unknown":
			return type_name
		else:
			return self.get_emoji(f"poke_{type_name}")

	async def get_pokemon_data(self, pokemon):
		# Sanitize input first
		pokemon = pokemon.lower()
		replacements = { " ": "-", "♂": "-m", "♀": "-f" }
		for key in replacements:
			pokemon = pokemon.replace(key, replacements[key])
		pokemon = re.sub(r'[^a-z0-9\-]', '', pokemon)

		words = pokemon.split("-")
		if "mega" in words:
			words.remove("mega")
			words.insert(1, "mega")
			pokemon = "-".join(words)

		try:
			form_data = await pokeapi_query(f"/pokemon-form/{pokemon}/")
			data = await pokeapi_query(form_data["pokemon"]["url"], True)
		except Http404Error as e:
			form_data = None
			data = await pokeapi_query(f"/pokemon/{pokemon}/")
		species_data = await pokeapi_query(data["species"]["url"], True)

		data["localized_name"] = localize(species_data["names"], "name")
		if data["id"] >= 1000:
			data["id"] = species_data["id"]
		data["wiki_url"] = f"http://www.serebii.net/pokedex-sm/{data['id']:03d}.shtml"


		if form_data:
			data["sprites"] = form_data["sprites"]
			name = localize(form_data["names"], "name")
			if name:
				data["localized_name"] = name
			if form_data.get("is_mega"):
				data["wiki_url"] += "#mega"

		return data, species_data

	# returns True on success, False on failure
	async def play_pokecry(self, ctx, poke_id, namestring, old=False):
		is_mega = "mega" in str(namestring).lower()

		name = str(poke_id)
		if old:
			name = f"old/{name}"
		if is_mega:
			name = f"{name}-mega"

		try:
			clip = await self.get_clip(f"url:http://dillerm.io/data/pokemon_cries/{name}.ogg", ctx)
			clip.volume = 0.1
			await self.play_clip(clip, ctx)
		except Http404Error:
			return False
		except AudioPlayerNotFoundError:
			return True

		return True


	@commands.command(aliases=["pokemon"])
	async def pokedex(self, ctx, *, pokemon):
		"""Looks up information about the indicated pokemon

		Pokemon should be specified using either their name or id number

		Clicking on the pokemon's name will bring you to their wiki page

		Example:
		`{cmdpfx}pokedex charizard`"""
		with ctx.channel.typing():
			data, species_data = await self.get_pokemon_data(pokemon)

		types = []
		for t in sorted(data["types"], key=lambda t: t["slot"]):
			types.append(self.poke_type(t["type"]["name"]))

		flavor_text = localize(species_data["flavor_text_entries"], "flavor_text")
		flavor_text = flavor_text.replace("\n", " ")

		embed = disnake.Embed(description=flavor_text, color=poke_color(species_data["color"]["name"]))
		embed.title = data["localized_name"] + f" #{data['id']}"
		embed.url = data["wiki_url"]

		embed.set_thumbnail(url=data["sprites"]["front_default"])

		embed.add_field(name=f"Type{'s' if len(types) > 1 else ''}", value=f"{''.join(types)}")
		if species_data.get("habitat"):
			embed.add_field(name="Habitat", value=f"{species_data['habitat']['name']}")
		embed.add_field(name="Weight", value=f"{data['weight'] / 10} kg")
		embed.add_field(name="Height", value=f"{data['height'] / 10} m")

		# fails silently if there's no cry for this pokemon
		await self.play_pokecry(ctx, data["id"], pokemon)

		await ctx.send(embed=embed)

	@commands.command()
	async def shiny(self, ctx, *, pokemon):
		"""Gets the shiny version of this pokemon

		pokemon should be specified using either their name or id number

		Example:
		`{cmdpfx}shiny charizard`"""

		# Sanitize input first
		with ctx.channel.typing():
			data, species_data = await self.get_pokemon_data(pokemon)

		if not data["sprites"].get("front_shiny"):
			await ctx.send("This pokemon doesn't have a shiny version")

		embed = disnake.Embed(color=poke_color(species_data["color"]["name"]))
		embed.set_image(url=data["sprites"].get("front_shiny"))

		# fails silently if there's no cry for this pokemon
		await self.play_pokecry(ctx, data["id"], pokemon)

		await ctx.send(embed=embed)
	
	@commands.command(aliases=["cry"])
	async def pokecry(self, ctx, *, pokemon):
		"""Plays the pokemon's sound effect

		Audio files for these pokemon cries were gotten from [Veekun](https://veekun.com/dex/downloads). Veekun does not have the cries for Generation VII yet, so I won't be able to play those.

		Most pokemon have a new cry and an old cry. To get the old cry instead of the new one, add 'old' to the end of your command (see example below.)

		**Example:**
		`{cmdpfx}pokecry pikachu`
		`{cmdpfx}pokecry bulbasaur old`"""
		words = pokemon.split(" ")
		old = "old" in words
		if old:
			words.remove("old") 
		pokemon = " ".join(words)

		data, species_data = await self.get_pokemon_data(pokemon)

		if data["id"] > 721:
			raise UserError("Sorry, I don't have the cries for pokemon in Generation VII yet")

		success = await self.play_pokecry(ctx, data["id"], pokemon, old=old)

		if not success:
			raise UserError(f"Couldn't find the cry for {data['localized_name']}") class Artifact(MangoCog):
	"""Artifact related commands
	
	A few commands providing information about Valve's new game [Artifact](https://playartifact.com)"""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)
		self.card_sets_uri = "artifact_card_sets"
		self.card_sets = []
		self.cards = []

	async def load_card_sets(self):
		card_sets_filename = httpgetter.cache.get_filename(self.card_sets_uri)

		if card_sets_filename is None:
			await self.update_card_sets()
			return

		self.card_sets = []
		self.cards = []
		for set_data in read_json(card_sets_filename):
			card_set = CardSet(set_data)
			self.card_sets.append(card_set)
			for card_data in set_data["card_list"]:
				self.cards.append(Card(card_data, card_set))
		for card in self.cards:
			card.update_references(self.get_card)

		self.card_names = OrderedDict()
		for card in self.cards:
			if card.name:
				if card.name.lower() not in self.card_names or card.large_image:
					self.card_names[card.name.lower()] = card.id

	async def update_card_sets(self):
		card_sets_data = []
		for n in range(0, 100):
			try:
				set_data = await httpgetter.get(f"https://playartifact.com/cardset/{str(n).zfill(2)}")
			except HttpError as e:
				if e.code == 400:
					break # this set doesnt exist yet
				else:
					raise
			except json.decoder.JSONDecodeError as e:
				break # valve has removed the api D:
			data = (await httpgetter.get(f"{set_data['cdn_root']}{set_data['url']}"))["card_set"]
			card_sets_data.append(data)

		card_sets_filename = await httpgetter.cache.new(self.card_sets_uri, "json")
		write_json(card_sets_filename, card_sets_data)

		await self.load_card_sets()

	def get_card(self, card_id):
		for card in self.cards:
			if card.id == card_id:
				return card
		return None

	# there is a better way to do this but im lazy and tired right now
	def find_card(self, name):
		if name == "":
			return None
		if name is None:
			return self.get_card(random.choice(list(self.card_names.values())))
		name = name.lower()
		for card_name in self.card_names:
			if name == card_name:
				return self.get_card(self.card_names[card_name])
		for card_name in self.card_names:
			if name in card_name.split(" "):
				return self.get_card(self.card_names[card_name])
		for card_name in self.card_names:
			if name in card_name:
				return self.get_card(self.card_names[card_name])
		pattern = r"[^a-z]"
		name = re.sub(pattern, "", name)
		if name == "":
			return None
		for card_name in self.card_names:
			if name == re.sub(pattern, "", card_name):
				return self.get_card(self.card_names[card_name])
		for card_name in self.card_names:
			if name in re.sub(pattern, "", card_name):
				return self.get_card(self.card_names[card_name])
		return None

	@checks.is_owner()
	@commands.command(hidden=True)
	async def updateartifact(self, ctx):
		"""Updates all the artifact card data"""
		await self.update_card_sets()
		await ctx.message.add_reaction("✅")


	@commands.command(aliases=["artifact"])
	async def card(self, ctx, *, card_name : str=None):
		"""Displays info about the artifact card
		
		Example:
		`{cmdpfx}artifact Healing Salve`
		`{cmdpfx}artifact Prowler Vanguard`
		`{cmdpfx}artifact keefe`"""
		card = self.find_card(card_name)

		if card is None:
			raise UserError(f"Couldn't find any card with the name '{card_name}'")

		embed = disnake.Embed(color=disnake.Color(card.color.integer))
		if card.mini_image:
			embed.set_author(name=card.name, icon_url=card.mini_image)
		else:
			embed.set_author(name=card.name)
		
		if card.large_image:
			embed.set_image(url=card.large_image)
		elif card.text:
			embed.description = card.text

		if card.references:
			descriptions = []
			for ref in card.references:
				ref_type = format_pascal_case(ref.ref_type.replace("_", " "))
				if ref.count:
					ref_type += f" (x{ref.count})"
				if ref.card.name == card.name:
					continue
				descriptions.append(f"{ref_type}: **{ref.card.name}**")
			if descriptions:
				embed.description = "\n".join(descriptions)

		embed.set_footer(text=f"Set: {card.set.name}")

		await ctx.send(embed=embed)

	@commands.command()
	async def deck(self, ctx, deck_string):
		"""Displays the card list for the given deck
		
		Example:
		`{cmdpfx}deck ADCJWkTZX05uwGDCRV4XQGy3QGLmqUBg4GQJgGLGgO7AaABR3JlZW4vQmxhY2sgRXhhbXBsZQ__`"""
		deck_info = ParseDeck(deck_string)
		if not deck_info:
			raise UserError("This doesn't look like a proper deck string")
		cards = []
		hero_turns = {}
		card_counts = {}
		for card_info in deck_info["heroes"]:
			card = self.get_card(card_info["id"])
			for ref in card.get_references("includes"):
				deck_info["cards"].append({ "id": ref.id, "count": ref.count })
			cards.append(card)
			hero_turns[card_info["id"]] = card_info["turn"]
		for card_info in deck_info["cards"]:
			cards.append(self.get_card(card_info["id"]))
			card_counts[card_info["id"]] = card_info["count"]

		cards = list(filter(lambda c: c is not None, cards))

		embed = disnake.Embed()

		embed.title = deck_info["name"]
		embed.url = f"https://playartifact.com/d/{deck_string}"

		deck_image = await drawdota.draw_artifact_deck(deck_string, cards, hero_turns, card_counts)
		deck_image = disnake.File(deck_image, "deck.png")
		embed.set_image(url=f"attachment://{deck_image.filename}")

		await ctx.send(embed=embed, file=deck_image) class Admin(MangoCog):
	"""Commands to help manage mangobyte on your server/guild
	
	Primarily used to stop people from ruining stuff, and to configure mangobyte to do different things.

	Only admins or people with the correct role can use these commands. To set a role as the 'Bot Admin' role, try `{cmdpfx}config botadmin`"""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)

	def bot_check(self, ctx):
		"""Checks to make sure the user has permissions"""
		guildinfo = botdata.guildinfo(ctx)
		if not isinstance(ctx.message.channel, disnake.abc.PrivateChannel):
			if guildinfo.is_banned(ctx.message.author):
				return False
			if guildinfo.is_disabled(ctx.command):
				return False
		return True

	def cog_check(self, ctx):
		"""Checks to make sure the user has admin privilages"""
		return checks.is_admin_check(ctx.message.channel, ctx)

	@commands.command()
	async def botban(self, ctx, user: disnake.Member):
		"""Bans the user from using commands"""
		if checks.is_owner_check(user):
			await ctx.send("Ya can't ban mah owner, man. 😠")
			return
		if checks.is_admin_check(ctx.message.channel, ctx, user):
			if not checks.is_owner_check(ctx.message.author):
				await ctx.send("Ya can't ban other admins")
				return
		if user.id == self.bot.user.id:
			await ctx.send("Lol you can't ban me, silly")
			return
		botdata.guildinfo(ctx.message.guild).botban(user)
		await ctx.send("{} has henceforth been banned from using commands 😤".format(user.mention))

	@commands.command()
	async def botunban(self, ctx, user: disnake.Member):
		"""Unbans the user, allowing them to use commands"""
		if checks.is_owner_check(user) or user == self.bot.user:
			await ctx.send("Ha ha. Very funny.")
			return
		botdata.guildinfo(ctx.message.guild).botunban(user)
		await ctx.send("{} is free of their restraints and may once again use commands".format(user.mention))

	def get_command_or_cog(self, ctx, text):
		result = ctx.bot.cogs.get(text)
		if result is not None:
			return result
		result = ctx.bot.get_command(text)
		if result is not None:
			return result
		text = text.lower()
		for cog in ctx.bot.cogs:
			if text == cog.lower():
				return ctx.bot.cogs[cog]
		return None

	@commands.command()
	async def disablecommand(self, ctx, command: str):
		"""Disabled the specified command or command category

		**Examples:**
		`{cmdpfx}disablecommand wiki`
		`{cmdpfx}disablecommand Audio`"""
		guildinfo = botdata.guildinfo(ctx)
		if not guildinfo:
			raise UserError("This command must be called in a guild")

		cmd = self.get_command_or_cog(ctx, command)
		if cmd is None:
			raise UserError("Couldn't find a command or command category by that name")

		secure_cogs = [ "Admin", "Owner" ]
		if isinstance(cmd, disnake.ext.commands.Command):
			if guildinfo.is_disabled(cmd.cog_name):
				raise UserError(f"The category this command belongs to ({cmd.cog_name}) is already disabled")
			if cmd.cog_name in secure_cogs:
				raise UserError("You can't disable a command in this category")
		else:
			if cmd.name in secure_cogs:
				raise UserError("You can't disable this category")

		if guildinfo.is_disabled(cmd.name):
			raise UserError("This has already been disabled")
		guildinfo.disable_command(cmd.name)
		await ctx.message.add_reaction("✅")

	@commands.command()
	async def enablecommand(self, ctx, command: str):
		"""Re-enables the specified command or command category

		Only works on commands that have already been disabled by the `{cmdpfx}disablecommand` command

		**Examples:**
		`{cmdpfx}enablecommand wiki`
		`{cmdpfx}enablecommand Audio`"""
		guildinfo = botdata.guildinfo(ctx)
		if not guildinfo:
			raise UserError("This command must be called in a guild")
		cmd = self.get_command_or_cog(ctx, command)
		if cmd is None:
			raise UserError("Couldn't find a command or command category by that name")
		if not guildinfo.is_disabled(cmd.name):
			if guildinfo.is_disabled(cmd):
				raise UserError(f"This command is not disabled, but its category ({cmd.cog_name}) is")
			else:
				raise UserError("This is not currently disabled")
		guildinfo.enable_command(cmd.name)
		await ctx.message.add_reaction("✅")


	@commands.command()
	async def summon(self, ctx, channel : str = None):
		"""Summons the bot to the voice channel

		You can specify the specific voice channel that you would like to connect to. If no channel is specified, it will connect to whatever channel you are currently in.
		**Examples:**
		`{cmdpfx}summon`
		`{cmdpfx}summon General`"""
		if channel:
			actual_channel = None
			if ctx.message.guild:
				for ch in ctx.message.guild.voice_channels:
					if channel.lower() == ch.name.lower():
						actual_channel = ch
						break
				if not actual_channel:
					for ch in ctx.message.guild.voice_channels:
						if channel.lower() in ch.name.lower():
							actual_channel = ch
							break
			channel = actual_channel

		if not channel:
			if not ctx.message.guild:
				raise UserError("You have to say that in a server")
			if not ctx.message.author.voice:
				raise UserError("You are not currently in a voice channel")
			channel = ctx.message.author.voice.channel
			if channel.guild != ctx.message.guild:
				raise UserError("You are not currently in a voice channel on this server/guild")

		audio = self.bot.get_cog("Audio")
		if not audio:
			raise UserError("You must have the Audio cog enabled to do this")
		try:
			await audio.connect_voice(channel)
			botdata.guildinfo(channel.guild.id).voicechannel = channel.id
		except asyncio.TimeoutError:
			cmdpfx = botdata.command_prefix(ctx)
			raise UserError(f"There was a timeout when attempting to do the `{cmdpfx}summon`")
		await ctx.message.add_reaction("✅")


	@commands.command()
	async def unsummon(self, ctx):
		"""Removes the bot from the voice channel"""
		audio = self.bot.get_cog("Audio")
		if not audio:
			raise UserError("You must have the Audio cog enabled to do this")
		if not ctx.message.guild:
			raise UserError("You have to say that in a server")
		await audio.disconnect(ctx.message.guild)
		botdata.guildinfo(ctx.message.guild.id).voicechannel = None
		await ctx.message.add_reaction("✅")


	@commands.command()
	async def resummon(self, ctx):
		"""Re-summons the bot to the voice channel

		This command is useful if you are having issues with mangobyte not being responsive"""
		audio = self.bot.get_cog("Audio")
		if not audio:
			raise UserError("You must have the Audio cog enabled to do this")
		if not ctx.message.guild:
			raise UserError("You have to be in a server to use this command")

		guildinfo = botdata.guildinfo(ctx.message.guild.id)

		save_channel = False
		channel = None
		if ctx.message.guild.me.voice:
			channel = ctx.message.guild.me.voice.channel
		elif ctx.message.author.voice:
			channel = ctx.message.author.voice.channel
		elif guildinfo.voicechannel is not None:
			channel = self.bot.get_channel(guildinfo.voicechannel)
		else:
			raise UserError("I'm not sure where you want me to resummon to. I'm not in any channel currently.")

		await audio.disconnect(ctx.message.guild)

		await asyncio.sleep(1)

		try:
			await audio.connect_voice(channel)
			guildinfo.voicechannel = channel.id
		except asyncio.TimeoutError:
			cmdpfx = botdata.command_prefix(ctx)
			raise UserError(f"There was a timeout when attempting to do the `{cmdpfx}summon`")

		await ctx.message.add_reaction("✅")

	@commands.command()
	async def config(self, ctx, name, *, value = None):
		"""Configures the bot's settings for this server

		Below are the different settings that you can tweak to customize mangobyte for this server. You can get more information about a setting by typing `{cmdpfx}config <settingname>`, and you can configure a setting by typing `{cmdpfx}config <settingname> <value>`

		{config_help}
		"""
		var = next((v for v in GuildInfo.variables if v["key"] == name), None)
		if not var:
			vars_list = "\n".join(map(lambda v: f"`{v['key']}`", GuildInfo.variables))
			await ctx.send(f"There is no config setting called '{name}'. Try one of these:\n{vars_list}")
			return
		
		currentvalue = botdata.guildinfo(ctx.guild)[var["key"]]
		if not value: # We are just getting a value
			await ctx.send(embed=await botdatatypes.localize_embed(ctx, var, currentvalue, f"{self.cmdpfx(ctx)}config"))
		else: # We are setting a value
			value = await botdatatypes.parse(ctx, var, value, currentvalue)
			botdata.guildinfo(ctx.guild)[var["key"]] = value
			await ctx.message.add_reaction("✅") class Owner(MangoCog):
	"""Owner commands
	
	Only used by the bot owner, for managing the bot"""
	def __init__(self, bot):
		MangoCog.__init__(self, bot)

	def cog_check(self, ctx):
		"""Checks to make sure the user is the bot owner"""
		return checks.is_owner_check(ctx.message.author)

	async def youtube_download(self, youtube_id, video_file):
		return await self.bot.loop.run_in_executor(ThreadPoolExecutor(max_workers=1), youtube_download_func, youtube_id, video_file)

	@commands.command()
	async def updateemoji(self, ctx, name=None):
		"""Updates the emoji information for the bot

		will also add new heroes to the emoji's if there are any

		passing in a name will target that emoji specifically"""
		emoji_json = read_json(settings.resource("json/emoji.json"))

		# find any heroes not currently added
		with ctx.channel.typing():
			dotabase = self.bot.get_cog("Dotabase")
			for hero in dotabase.session.query(Hero):
				emoji_name =f"dota_hero_{hero.name}"
				if emoji_name not in emoji_json:
					# missing this hero, gotta add it
					await ctx.send(f"Adding emoji for {hero.localized_name}")
					url = dotabase.vpkurl + hero.icon
					image = await httpgetter.get(url, return_type="filename", cache=True)
					with open(image, "rb") as f:
						image = f.read()
					emoji = await ctx.guild.create_custom_emoji(name=emoji_name, image=image, reason=f"New Hero got added")
					emoji_json[emoji_name] = emoji.id

			for emoji in ctx.guild.emojis:
				if name is None or name == emoji.name:
					imgpath = settings.resource(f"images/emojis/{emoji.name}.png")
					with open(imgpath, 'wb+') as f:
						f.write((await httpgetter.get(str(emoji.url), return_type="bytes")).read())
					emoji_json[emoji.name] = emoji.id
		write_json(settings.resource("json/emoji.json"), emoji_json)
		await ctx.send("done!")


	@commands.command()
	async def addclip(self, ctx, url, clipname, start, end, start_fade=0.25, end_fade=0.25):
		"""Adds a clip from youtube"""
		outfile = settings.resource(f"clips/{clipname}.mp3")
		start = get_time(start)
		end = get_time(end)
		duration = end - start

		matches = [
			re.match(r"https?://(?:www\.)?youtube\.com/watch\?v=([^/]*)", url),
			re.match(r"https?://(?:www\.)?youtu\.be/([^/]*)", url),
			re.match(r"([^/]*)", url)
		]
		youtube_id = None
		for match in matches:
			if match:
				youtube_id = match.group(1)
				break
		if youtube_id is None:
			raise UserError("This doesnt look like a youtube url or an id")

		video_file = settings.resource(f"cache/youtube/{youtube_id}.mp3")

		if not os.path.exists(video_file):
			await self.youtube_download(youtube_id, video_file)

		fadefilter = f"afade=t=in:ss=0:d={start_fade},afade=t=out:st={duration - end_fade}:d={end_fade}"

		# Converting / Cropping
		run_command(["ffmpeg", "-ss", str(start), "-t", str(duration), "-y", "-i", video_file, "-af", fadefilter, outfile ])

		audio = self.bot.get_cog("Audio")
		audio.local_clipinfo[clipname] = OrderedDict([
			('path', f"{clipname}.mp3"),
			('source', f"https://www.youtube.com/watch?v={youtube_id}"),
			('start', start),
			('end', end)
		])
		audio.save_local_clipinfo()

		# Playing
		await self.play_clip(f"local:{clipname}", ctx)

	@commands.command(aliases=["editclip"])
	async def editclipinfo(self, ctx, clipname, attribute, *, value):
		"""Allows editing of a clip's info

		warning: volume actually edits the clip, and is a multiplier (0.5 cuts in half, 2 doubles)

		Example:
		`{cmdpfx}editclipinfo wow text Waow!`"""
		audio = self.bot.get_cog("Audio")
		if clipname not in audio.local_clipinfo:
			raise UserError("That clip doesn't exist")

		attribs = [
			"text",
			"author",
			"source",
			"start",
			"end"
		]

		if value is None or value == "":
			raise UserError("Gotta gimme a value")

		if attribute == "volume":
			filename = settings.resource("clips/" + audio.local_clipinfo[clipname]["path"])
			temp_file = settings.resource(f"temp/temp_{clipname}" + os.path.splitext(filename)[1])
			run_command(["ffmpeg", "-i", filename, "-af", f"volume={value}", temp_file])
			shutil.copyfile(temp_file, filename)
			os.remove(temp_file)
			await self.play_clip(f"local:{clipname}", ctx)
			await ctx.message.add_reaction("✅")
			return

		if attribute not in attribs:
			attribs_string = "\n".join(attribs)
			raise UserError(f"Invalid attribute name, try one of these:```\n{attribs_string}\n```")

		audio.local_clipinfo[clipname][attribute] = value
		audio.save_local_clipinfo()
		await ctx.message.add_reaction("✅")

	@commands.command(aliases=["botdata"])
	async def getbotdata(self, ctx, selector, identifier : int):
		"""Gets info about a user or a server"""
		if selector in ["user", "player", "member"]:
			data = botdata.userinfo(identifier)
			try:
				user = await self.bot.fetch_user(identifier)
			except:
				raise UserError("Couldn't find that user")

			embed = disnake.Embed(description=(user.mention + "\n```json\n" + json.dumps(data.json_data, indent='\t') + "\n```"))
			embed.set_thumbnail(url=user.avatar.url)
			if data.steam:
				embed.add_field(name="Profiles", value=(
					f"[Steam](http://steamcommunity.com/id/{data.steam})\n"
					f"[OpenDota](https://www.opendota.com/players/{data.steam})\n"))
			await ctx.send(embed=embed)
		elif selector in ["server", "guild"]:
			data = botdata.guildinfo(identifier)
			guild = self.bot.get_guild(identifier)
			if guild is None:
				raise UserError("Couldn't find that guild")
			invite = None
			for channel in guild.text_channels:
				if channel.permissions_for(guild.me).create_instant_invite:
					invite = await channel.create_invite()
					break

			embed = disnake.Embed(description=("```json\n" + json.dumps(data.json_data, indent='\t') + "\n```"))
			embed.set_author(name=guild.name)
			if guild.icon_url != "":
				embed.set_thumbnail(url=guild.icon_url)
			embed.add_field(name="Region", value=guild.region)
			embed.add_field(name="Member Count", value=guild.member_count)
			if invite:
				embed.add_field(name="Invite", value=invite.url)
			await ctx.send(embed=embed)

	@commands.command()
	async def clearcache(self, ctx, uri):
		"""Clears the cache at the given uri

		or clears everything from cache if given the 'all' keyword"""
		try:
			await httpgetter.cache.remove(uri)
		except KeyError:
			raise UserError("Couldn't find a cached version of that")

		await ctx.message.add_reaction("✅")

	@commands.command()
	async def getcache(self, ctx, uri):
		"""Gets a file in the cache"""
		filename = httpgetter.cache.get_filename(uri)

		if filename is None:
			raise UserError("Couldn't find a file at that uri")

		await ctx.send(file=disnake.File(filename))

	@commands.command()
	async def errors(self, ctx, count : int=5, page : int=0, excludestring=None):
		"""Gets a list of the most recent errors from loggingdb"""

		for error in loggingdb.session.query(loggingdb_spec.Error).order_by(desc(loggingdb_spec.Error.timestamp)).offset(page * count):
			if count <= 0:
				return
			error_chunks = error.error_text_chunks()
			try:
				if excludestring and re.search(excludestring, "\n".join(error_chunks)):
					continue # skip this because our excludestring matches
			except Exception:
				raise UserError("Regex parsing error")
			await ctx.send(embed=error.to_embed(self))
			for chunk in error_chunks[:-1]:
				await ctx.send(chunk)
				await asyncio.sleep(0.5)

			await ctx.send(error_chunks[-1] + "\n_ _")
			await asyncio.sleep(0.5)
			count -= 1

	@commands.command()
	async def remoteresummon(self, ctx, guild_id : int):
		"""Re-summons the bot for the given guild

		This command is useful if you are having issues with mangobyte not being responsive"""
		audio = self.bot.get_cog("Audio")
		if not audio:
			raise UserError("You must have the Audio cog enabled to do this")

		guild = self.bot.get_guild(guild_id)

		if guild is None:
			raise UserError(f"guild '{guild_id}' not found")

		guildinfo = botdata.guildinfo(guild_id)

		channel = None
		if guild.me.voice:
			channel = guild.me.voice.channel
		elif guildinfo.voicechannel is not None:
			channel = self.bot.get_channel(guildinfo.voicechannel)
		else:
			raise UserError("I'm not sure where you want me to resummon to. I'm not in any channel currently.")

		await audio.disconnect(guild)

		await asyncio.sleep(1)

		try:
			await audio.connect_voice(channel)
			guildinfo.voicechannel = channel.id
		except asyncio.TimeoutError:
			cmdpfx = botdata.command_prefix(ctx)
			raise UserError(f"There was a timeout when attempting to do the `{cmdpfx}summon`")

		await ctx.message.add_reaction("✅")


	@commands.command(aliases=["logs", "logger"])
	async def loggingdb(self, ctx, table, identifier):
		"""Gets a list of the most recent errors from loggingdb"""
		if table in [ "error", "errors", "bugs", "bug" ]:
			table = loggingdb_spec.Error
			filterer = lambda q: q.filter_by(message_id=identifier)
		elif table in [ "message", "messages", "msg", "messages" ]:
			table = loggingdb_spec.Message
			filterer = lambda q: q.filter_by(id=identifier)
		elif table in [ "command", "commands", "cmd", "cmds" ]:
			table = loggingdb_spec.Command
			filterer = lambda q: q.filter_by(message_id=identifier)
		else:
			raise UserError("Dont know what table you're talking about")
		found = False
		for obj in filterer(loggingdb.session.query(table)):
			found = True
			await ctx.send(embed=obj.to_embed(self))
			if table == loggingdb_spec.Error:
				chunks = obj.error_text_chunks()
				for chunk in chunks:
					await ctx.send(chunk)
		if not found:
			raise UserError("Couldn't find anything for that")

	@commands.command(aliases=["restart", "quit", "kill", "pokemango", "exit"])
	async def close(self, ctx):
		"""Kills the bot"""
		await self.bot.change_presence(status=disnake.Status.offline)
		await self.bot.close()
from argparse import ArgumentError
from cogs.utils.settings import Settings
from cogs.utils.logger import setup_logger
from cogs.utils.botdata import BotData
from cogs.utils.loggingdb import LoggingDb
from cogs.utils.httpgetter import HttpGetter
from cogs.utils.helpers import *
from disnake.ext import commands
from cogs.utils.helpcommand import MangoHelpCommand
from cogs.utils.clip import *
from cogs.utils.commandargs import *
from cogs.general import General
from cogs.audio import Audio
from cogs.dotabase import Dotabase
from cogs.dotastats import DotaStats
from cogs.pokemon import Pokemon
from cogs.artifact import Artifact
from cogs.admin import Admin
from cogs.owner import Owner
import disnake
import traceback
import asyncio
import datetime
import json
import sys
import inspect
			pass

error_file = "errors.json"

async def print_missing_perms(ctx, error):
	if not (ctx.guild):
		await ctx.send("Uh-oh, sumthin dun gone wrong 😱")
		trace_string = await report_error(ctx.message, error, skip_lines=0)
	my_perms = ctx.channel.permissions_for(ctx.guild.me)
	perms_strings = read_json(settings.resource("json/permissions.json"))
	perms = []
	for i in range(0, 32):
		if ((permissions >> i) & 1) and not ((permissions >> i) & 1):
			words = perms_strings["0x{:08x}".format(1 << i)].split("_")
			for i in range(0, len(words)):
				words[i] = f"**{words[i][0] + words[i][1:].lower()}**"
			perms.append(" ".join(words))
	if perms:
		await ctx.send("Looks like I'm missin' these permissions 😢:\n" + "\n".join(perms))
	else:
		await ctx.send(f"Looks like I'm missing permissions 😢. Have an admin giff me back my permissions, or re-invite me to the server using this invite link: {invite_link}")


async def report_error(message, error, skip_lines=2):
	if os.path.isfile(error_file):
		error_list = read_json(error_file)
	else:
		error_list = []

	try:
		raise error.original
	except:
		trace = traceback.format_exc().replace("\"", "'").split("\n")
		if skip_lines > 0 and len(trace) >= (2 + skip_lines):
			del trace[1:(skip_lines + 1)]
		trace = [x for x in trace if x] # removes empty lines

	trace_string = "\n".join(trace)

	await loggingdb.insert_error(message, error, trace_string)

	logger.error(f"\nError on: {message.content}\nAuthor Id: {message.author.id}\n{trace_string}\n")
	return trace_string

def update_commandinfo():
	commands_file = "resource/json/commands.json"
	data = {
		"cogs": [],
		"commands": []
	}
	for cmd in bot.commands:
		if cmd.cog and cmd.cog.name == "Owner":
			continue
		data["commands"].append({
			"name": cmd.name,
			"signature": bot.help_command.get_command_signature(cmd),
			"short_help": cmd.short_doc,
			"help": bot.help_command.fill_template(cmd.help),
			"aliases": cmd.aliases,
			"cog": cmd.cog.name if cmd.cog else "General",
			"prefix": "?"
		})
	for cmd in bot.slash_commands:
		data["commands"].append({
			"name": cmd.name,
			"signature": None,
			"short_help": cmd.description,
			"help": cmd.description,
			"aliases": [],
			"cog": cmd.cog.name if cmd.cog else "General",
			"prefix": "/"
		})
	for cog in bot.cogs:
		if cog == "Owner":
			continue
		data["cogs"].append({
			"name": cog,
			"short_help": bot.help_command.cog_short_doc(bot.cogs[cog]),
			"help":  inspect.getdoc(bot.cogs[cog])
		})
	data["commands"] = list(sorted(data["commands"], key=lambda c: c["name"]))

	with open(commands_file, "w+") as f:
		f.write(json.dumps(data, indent="\t"))

	max_command_len = max(map(lambda c: len(c["name"]), data["commands"]))
	max_short_help_len = max(map(lambda c: len(c["short_help"]), data["commands"]))

	docs = ""
	docs += f"Mangobyte currently has {len(data['commands'])} commands, separated into {len(data['cogs'])} categories\n"
	for cog in data["cogs"]:
		docs += f"\n#### {cog['name']}\n"
		docs += f"{cog['short_help']}\n"
		docs += "\n```\n"
		for cmd in data["commands"]:
			if cmd["cog"] == cog["name"]:
				docs += f"{cmd['prefix']}{cmd['name']: <{max_command_len + 1}} | {cmd['short_help']: <{max_short_help_len + 1}}\n"
		docs += "```\n"

	readme_file = "README.md"
	readme_replacement_start = "<!-- COMMANDS_START -->\n"
	readme_replacement_end = "\n<!-- COMMANDS_END -->"
	with open(readme_file, "r") as f:
		text = f.read()
	text = re.sub(f"({readme_replacement_start}).*({readme_replacement_end})", f"\\1{docs}\\2", text, flags=re.S)
	with open(readme_file, "w+") as f:
		f.write(text)

	logger.info("done!")



if __name__ == '__main__':
	bot.add_cog(General(bot))
	bot.add_cog(Audio(bot))
	bot.add_cog(Dotabase(bot))
	bot.add_cog(DotaStats(bot))
	bot.add_cog(Pokemon(bot))
	bot.add_cog(Artifact(bot))
	bot.add_cog(Admin(bot))

	bot.add_cog(Owner(bot))