import discord from discord.ext import commands from . import utils import random import re import calendar import pendulum import datetime import psutil class Miscallaneous: """Core commands, these are the miscallaneous commands that don't fit into other categories'""" def __init__(self, bot): self.bot = bot self.process = psutil.Process() self.process.cpu_percent() def _command_signature(self, cmd): result = [cmd.qualified_name] if cmd.usage: result.append(cmd.usage) return ' '.join(result) params = cmd.clean_params if not params: return ' '.join(result) for name, param in params.items(): if param.default is not param.empty: # We don't want None or '' to trigger the [name=value] case and instead it should # do [name] since [name=None] or [name=] are not exactly useful for the user. should_print = param.default if isinstance(param.default, str) else param.default is not None if should_print: result.append(f'[{name}={param.default!r}]') else: result.append(f'[{name}]') elif param.kind == param.VAR_POSITIONAL: result.append(f'[{name}...]') else: result.append(f'<{name}>') return ' '.join(result) @commands.command() @commands.cooldown(1, 3, commands.cooldowns.BucketType.user) @utils.can_run(send_messages=True) async def help(self, ctx, *, entity: str = None): chunks = [] if entity: entity = self.bot.get_cog(entity) or self.bot.get_command(entity) if entity is None: fmt = "Hello! Here is a list of the sections of commands that I have (there are a lot of commands so just start with the sections...I know, I'm pretty great)\n" fmt += "To use a command's paramaters, you need to know the notation for them:\n" fmt += "\t This means the argument is __**required**__.\n" fmt += "\t[argument] This means the argument is __**optional**__.\n" fmt += "\t[A|B] This means the it can be __**either A or B**__.\n" fmt += "\t[argument...] This means you can have multiple arguments.\n" fmt += "\n**Type `{}help section` to get help on a specific section**\n".format(ctx.prefix) fmt += "**CASE MATTERS** Sections are in `Title Case` and commands are in `lower case`\n\n" chunks.append(fmt) cogs = sorted(self.bot.cogs.values(), key=lambda c: c.__class__.__name__) for cog in cogs: tmp = "**{}**\n".format(cog.__class__.__name__) if cog.__doc__: tmp += "\t{}\n".format(cog.__doc__) if len(chunks[len(chunks) - 1] + tmp) > 2000: chunks.append(tmp) else: chunks[len(chunks) - 1] += tmp elif isinstance(entity, (commands.core.Command, commands.core.Group)): tmp = "**{}**".format(self._command_signature(entity)) tmp += "\n{}".format(entity.help) chunks.append(tmp) else: cmds = sorted(ctx.bot.get_cog_commands(entity.__class__.__name__), key=lambda c: c.name) fmt = "Here are a list of commands under the section {}\n".format(entity.__class__.__name__) fmt += "Type `{}help command` to get more help on a specific command\n\n".format(ctx.prefix) chunks.append(fmt) for command in cmds: for subcommand in utils.get_all_subcommands(command): tmp = "**{}**\n\t{}\n".format(subcommand.qualified_name, subcommand.short_doc) if len(chunks[len(chunks) - 1] + tmp) > 2000: chunks.append(tmp) else: chunks[len(chunks) - 1] += tmp if utils.dev_server: tmp = "\n\nIf I'm having issues, then please visit the dev server and ask for help. {}".format(utils.dev_server) if len(chunks[len(chunks) - 1] + tmp) > 2000: chunks.append(tmp) else: chunks[len(chunks) - 1] += tmp if len(chunks) == 1 and len(chunks[0]) < 1000: destination = ctx.channel else: destination = ctx.author try: for chunk in chunks: await destination.send(chunk) except (discord.Forbidden, discord.HTTPException): await ctx.send("I cannot DM you, please allow DM's from this server to run this command") else: if ctx.guild and destination == ctx.author: await ctx.send("I have just DM'd you some information about me!") @commands.command() @utils.can_run(send_messages=True) async def ping(self, ctx): """Returns the latency between the server websocket, and between reading messages""" msg_latency = datetime.datetime.utcnow() - ctx.message.created_at fmt = "Message latency {0:.2f} seconds".format(msg_latency.seconds + msg_latency.microseconds / 1000000) fmt += "\nWebsocket latency {0:.2f} seconds".format(self.bot.latency) await ctx.send(fmt) @commands.command(aliases=["coin"]) @utils.can_run(send_messages=True) async def coinflip(self, ctx): """Flips a coin and responds with either heads or tails EXAMPLE: !coinflip RESULT: Heads!""" result = "Heads!" if random.SystemRandom().randint(0, 1) else "Tails!" await ctx.send(result) @commands.command() @utils.can_run(send_messages=True) async def say(self, ctx, *, msg: str): """Tells the bot to repeat what you say EXAMPLE: !say I really like orange juice RESULT: I really like orange juice""" fmt = "\u200B{}".format(msg) await ctx.send(fmt) try: await ctx.message.delete() except Exception: pass @commands.command() @utils.can_run(send_messages=True) async def calendar(self, ctx, month: str = None, year: int = None): """Provides a printout of the current month's calendar Provide month and year to print the calendar of that year and month EXAMPLE: !calendar january 2011""" # calendar takes in a number for the month, not the words # so we need this dictionary to transform the word to the number months = { "january": 1, "february": 2, "march": 3, "april": 4, "may": 5, "june": 6, "july": 7, "august": 8, "september": 9, "october": 10, "november": 11, "december": 12 } # In month was not passed, use the current month if month is None: month = datetime.date.today().month else: month = months.get(month.lower()) if month is None: await ctx.send("Please provide a valid Month!") return # If year was not passed, use the current year if year is None: year = datetime.datetime.today().year # Here we create the actual "text" calendar that we are printing cal = calendar.TextCalendar().formatmonth(year, month) await ctx.send("```\n{}```".format(cal)) @commands.command(aliases=['about']) @utils.can_run(send_messages=True) async def info(self, ctx): """This command can be used to print out some of my information""" # fmt is a dictionary so we can set the key to it's output, then print both # The only real use of doing it this way is easier editing if the info # in this command is changed # Create the original embed object # Set the description include dev server (should be required) and the optional patreon link description = "[Dev Server]({})".format(utils.dev_server) if utils.patreon_link: description += "\n[Patreon]({})".format(utils.patreon_link) # Now creat the object opts = {'title': 'Bonfire', 'description': description, 'colour': discord.Colour.green()} # Set the owner embed = discord.Embed(**opts) if hasattr(self.bot, 'owner'): embed.set_author(name=str(self.bot.owner), icon_url=self.bot.owner.avatar_url) # Setup the process statistics name = "Process statistics" value = "" memory_usage = self.process.memory_full_info().uss / 1024 ** 2 cpu_usage = self.process.cpu_percent() / psutil.cpu_count() value += 'Memory: {:.2f} MiB'.format(memory_usage) value += '\nCPU: {}%'.format(cpu_usage) if hasattr(self.bot, 'uptime'): value += "\nUptime: {}".format((pendulum.now(tz="UTC") - self.bot.uptime).in_words()) embed.add_field(name=name, value=value, inline=False) # Setup the user and guild statistics name = "User/Guild statistics" value = "" value += "Channels: {}".format(len(list(self.bot.get_all_channels()))) value += "\nUsers: {}".format(len(self.bot.users)) value += "\nServers: {}".format(len(self.bot.guilds)) embed.add_field(name=name, value=value, inline=False) # The game statistics name = "Game statistics" # To get the newlines right, since we're not sure what will and won't be included # Lets make this one a list and join it at the end value = [] hm = self.bot.get_cog('Hangman') ttt = self.bot.get_cog('TicTacToe') bj = self.bot.get_cog('Blackjack') interaction = self.bot.get_cog('Interaction') if hm: value.append("Hangman games: {}".format(len(hm.games))) if ttt: value.append("TicTacToe games: {}".format(len(ttt.boards))) if bj: value.append("Blackjack games: {}".format(len(bj.games))) if interaction: count_battles = 0 for battles in self.bot.get_cog('Interaction').battles.values(): count_battles += len(battles) value.append("Battles running: {}".format(len(bj.games))) embed.add_field(name=name, value="\n".join(value), inline=False) await ctx.send(embed=embed) @commands.command() @utils.can_run(send_messages=True) async def uptime(self, ctx): """Provides a printout of the current bot's uptime EXAMPLE: !uptime RESULT: A BAJILLION DAYS""" if hasattr(self.bot, 'uptime'): await ctx.send("Uptime: ```\n{}```".format((pendulum.now(tz="UTC") - self.bot.uptime).in_words())) else: await ctx.send("I've just restarted and not quite ready yet...gimme time I'm not a morning pony :c") @commands.command(aliases=['invite']) @utils.can_run(send_messages=True) async def addbot(self, ctx): """Provides a link that you can use to add me to a server EXAMPLE: !addbot RESULT: http://discord.gg/yo_mama""" perms = discord.Permissions.none() perms.read_messages = True perms.send_messages = True perms.manage_roles = True perms.ban_members = True perms.kick_members = True perms.manage_messages = True perms.embed_links = True perms.read_message_history = True perms.attach_files = True perms.speak = True perms.connect = True perms.attach_files = True perms.add_reactions = True app_info = await self.bot.application_info() await ctx.send("Use this URL to add me to a server that you'd like!\n<{}>" .format(discord.utils.oauth_url(app_info.id, perms))) @commands.command(enabled=False) @utils.can_run(send_messages=True) async def joke(self, ctx): """Prints a random riddle EXAMPLE: !joke RESULT: An absolutely terrible joke.""" # Currently disabled until I can find a free API pass @commands.command() @utils.can_run(send_messages=True) async def roll(self, ctx, *, notation: str = "d6"): """Rolls a die based on the notation given Format should be #d# EXAMPLE: !roll d50 RESULT: 51 :^)""" # Use regex to get the notation based on what was provided try: # We do not want to try to convert the dice, because we want d# to # be a valid notation dice = re.search("(\d*)d(\d*)", notation).group(1) num = int(re.search("(\d*)d(\d*)", notation).group(2)) # Attempt to get addition/subtraction add = re.search("\+ ?(\d+)", notation) subtract = re.search("- ?(\d+)", notation) # Check if something like ed3 was provided, or something else entirely # was provided except (AttributeError, ValueError): await ctx.send("Please provide the die notation in #d#!") return # Dice will be None if d# was provided, assume this means 1d# dice = dice or 1 # Since we did not try to convert to int before, do it now after we # have it set dice = int(dice) if dice > 30: await ctx.send("I'm not rolling more than 30 dice, I have tiny hands") return if num > 100: await ctx.send("What die has more than 100 sides? Please, calm down") return if num <= 1: await ctx.send("A {} sided die? You know that's impossible right?".format(num)) return nums = [random.SystemRandom().randint(1, num) for _ in range(0, int(dice))] subtotal = total = sum(nums) # After totalling, if we have add/subtract seperately, apply them if add: add = int(add.group(1)) total += add if subtract: subtract = int(subtract.group(1)) total -= subtract value_str = ", ".join("{}".format(x) for x in nums) if dice == 1: fmt = '{0.message.author.name} has rolled a {1} sided die and got the number {2}!'.format( ctx, num, value_str ) if add or subtract: fmt += "\nTotal: {} ({}".format(total, subtotal) if add: fmt += " + {}".format(add) if subtract: fmt += " - {}".format(subtract) fmt += ")" else: fmt = '{0.message.author.name} has rolled {1}, {2} sided dice and got the numbers {3}!'.format( ctx, dice, num, value_str ) if add or subtract: fmt += "\nTotal: {} ({}".format(total, subtotal) if add: fmt += " + {}".format(add) if subtract: fmt += " - {}".format(subtract) fmt += ")" else: fmt += "\nTotal: {}".format(total) await ctx.send(fmt) def setup(bot): bot.add_cog(Miscallaneous(bot))