def save_data(bot, force=False): """Saves all of the current data in the data dictionary. Does not save volatile_data, though. Backs up data if forced. """ if bot.data_changed or force: # Only save if something changed or forced # Loop through keys directory = bot.path + '/data/' if force: # Save all data for key, value in bot.data.items(): with open(directory + key + '.json', 'w') as current_file: try: json.dump(value, current_file, indent=4) except TypeError as e: logger.error('Failed to save data for %s: (TypeError) %s', key, e) # Check to see if any guild was removed files = os.listdir(directory) for check_file in files: if check_file.endswith('.json') and check_file[:-5] not in bot.data: logger.debug("Removing file {}".format(check_file)) os.remove(directory + check_file) else: # Save data that has changed for key in bot.data_changed: with open(directory + key + '.json', 'w') as current_file: json.dump(bot.data[key], current_file, indent=4) logger.debug("Saved {}".format(directory + key + '.json')) bot.data_changed = [] if force: utilities.make_backup(bot)
def __init__(self, error_subject, error_details, *args, e=None, error_type=ErrorTypes.RECOVERABLE, edit_pair=None, autodelete=0, use_embed=True, embed_fields=[], serious=False): self.error_type = error_type self.error_subject = str(error_subject) self.error_details = str(error_details) self.error_other = args self.provided_exception = e self.autodelete = autodelete self.use_embed = use_embed self.traceback = '' other_details = '\n'.join([str(arg) for arg in args]) self.error_message = "`{subject} error: {details}`\n{others}".format( subject=self.error_subject, details=self.error_details, others=other_details) emoji = ':warning:' if serious or random() > 0.01 else ':thinking:' self.embed = Embed(title='{} {} error'.format(emoji, self.error_subject), description='{}\n{}'.format(self.error_details, other_details), colour=Colour(0xffcc4d)) if e: if isinstance(e, BotException): given_error = '{}'.format(e.error_details) embed_fields = e.embed_fields else: given_error = '`{}: {}`'.format(type(e).__name__, e) self.error_message += '\nGiven error:\n{}'.format(given_error) embed_fields = [('Given error:', given_error)] + embed_fields if e.__traceback__: self.traceback = traceback.format_tb(e.__traceback__) else: self.traceback = traceback.format_exc() self.embed_fields = embed_fields for name, value in embed_fields: self.embed.add_field(name=name, value=value, inline=False) logger.error(self.error_message) # If non-recoverable, quit if error_type in (ErrorTypes.STARTUP, ErrorTypes.FATAL, ErrorTypes.INTERNAL): traceback.print_exc() sys.exit() if edit_pair: bot, message_reference = edit_pair asyncio.ensure_future( bot.edit_message(message_reference, self.error_message))
def broadcast_event(bot, event, *args, **kwargs): """ Loops through all of the plugins and looks to see if the event index specified is associated it. If it is, call that function with args. """ for plugin in bot.plugins.values(): function = getattr(plugin, event, None) if function: try: asyncio.ensure_future(function(bot, *args, **kwargs)) except TypeError as e: logger.error("Bypassing event error: %s", e) logger.error(traceback.format_exc())
def exception_handler(loop, context): e = context.get('exception') if e and e.__traceback__: traceback_text = ''.join(traceback.format_tb(e.__traceback__)) else: traceback_text = traceback.format_exc() if not traceback_text: traceback_text = '(No traceback available)' error_message = '{}\n{}'.format(e, traceback_text) logger.error("An uncaught exception occurred.\n" + error_message) with open(path + '/temp/error.txt', 'w') as error_file: error_file.write(error_message) logger.error("Error file written.") if bot.is_closed(): safe_exit()
def exception_handler(loop, context): e = context.get('exception') if e and e.__traceback__: traceback_text = ''.join(traceback.format_tb(e.__traceback__)) else: traceback_text = traceback.format_exc() if not traceback_text: traceback_text = '(No traceback available)' error_message = '{}\n{}'.format(e, traceback_text) logger.error("An uncaught exception occurred.\n%s", error_message) with open(path + '/temp/error.txt', 'w') as error_file: error_file.write(error_message) logger.error("Error file written.") if bot.is_closed(): safe_exit()
def safe_exit(): loop = asyncio.get_event_loop() try: # From discord.py client.run loop.run_until_complete(bot.logout()) pending = asyncio.Task.all_tasks() gathered = asyncio.gather(*pending) except Exception as e: logger.error("Failed to log out. %s", e) try: gathered.cancel() loop.run_until_complete(gathered) gathered.exception() except: pass logger.warn("Bot disconnected. Shutting down...") bot.shutdown() # Calls sys.exit
async def backup_loop(self): """Runs the loop that periodically backs up data (hours).""" try: interval = int(self.configurations['core']['backup_interval']) interval = 0 if interval <= 0 else interval * 3600 except: logger.warn("Backup interval not configured - backup loop stopped.") return channel_id = self.configurations['core']['debug_channel'] debug_channel = self.get_channel(channel_id) while not debug_channel: logger.warn("Debug channel not found. Trying again in 60 seconds...") await asyncio.sleep(60) debug_channel = self.get_channel(channel_id) while interval: utilities.make_backup(self) discord_file = discord.File('{}/temp/backup1.zip'.format(self.path)) try: await debug_channel.send(file=discord_file) except Exception as e: logger.error("Failed to upload backup file! %s", e) await asyncio.sleep(interval)
def broadcast_event(bot, event, *args, **kwargs): """Calls functions registered to the given event.""" if not bot.ready: return for function in bot.event_functions.get(event, []): try: asyncio.ensure_future(function(bot, *args, **kwargs)) except TypeError as e: logger.error("Bypassing event error: %s", e) logger.error(traceback.format_exc()) for function in bot.event_functions.get('all', []): try: asyncio.ensure_future(function(bot, event, *args, **kwargs)) except TypeError as e: logger.error("Bypassing event error: %s", e) logger.error(traceback.format_exc())
async def notify_owners(bot, message, user_id=None): """Sends all owners a direct message with the given text. If user_id is specified, this will check that the user is not in the blacklist. """ if bot.selfbot: logger.info("Owner notification:\n{}".format(message)) else: if user_id: blacklist = data.get(bot, 'core', 'blacklist', default=[]) if user_id in blacklist: await asyncio.sleep(0.5) return for owner in bot.owners: try: member = data.get_member(bot, owner) if len(message) > 1990: await send_text_as_file(member, message, 'notification') else: await member.send(message) except Exception as e: logger.error("Failed to notify owner %s: %s", owner, e)
async def handle_error( self, error, message, context, response, edit=None, command_editable=False): """Common error handler for sending responses.""" send_function = edit.edit if edit else message.channel.send self.last_exception = error if response.message and response.message.reactions: try: await response.message.clear_reactions() except: # No permissions pass if isinstance(error, BotException): self.last_traceback = error.traceback plugins.broadcast_event(self, 'bot_on_exception', error, message) content, embed = ('', error.embed) if error.use_embed else (str(error), None) if command_editable and error.autodelete == 0 and error.editable: if content: content += '\n\n(Note: The issuing command can be edited)' elif embed: embed.set_footer( text="\u200b\u200b\u200bThe issuing command can be edited", icon_url="http://i.imgur.com/fM9yGzI.png") # TODO: Handle long messages message_reference = await send_function(content=content, embed=embed) if error.autodelete > 0: await asyncio.sleep(error.autodelete) try: # Avoid delete_messages for selfbot mode message_reference = edit if edit else message_reference await message_reference.delete() await message.delete() except: pass return elif isinstance(error, discord.Forbidden): plugins.broadcast_event(self, 'bot_on_discord_exception', error, message) message_reference = None try: await message.author.send( content="Sorry, I don't have permission to carry " "out that command in that channel. The bot may have had " "its `Send Messages` permission revoked (or any other " "necessary permissions, like `Embed Links`, `Manage Messages`, or " "`Speak`).\nIf you are a bot moderator or server owner, " "you can mute channels with `{}mod mute <channel>` " "instead of using permissions directly. If that's not the " "issue, be sure to check that the bot has the proper " "permissions on the server and each channel!".format( self.command_invokers[0])) except: # User has blocked the bot pass # TODO: Consider sending a general permissions error else: if isinstance(error, discord.HTTPException) and len(str(response)) > 1998: plugins.broadcast_event(self, 'bot_on_discord_exception', error, message) message_reference = await utilities.send_text_as_file( message.channel, str(response), 'response', extra="The response is too long. Here is a text file of the contents.") else: insult = random.choice(self.exception_messages) error = '**`{0}:`**`{1}`'.format(type(error).__name__, error) embed = discord.Embed( title=':x: Internal error', description=insult, color=discord.Color(0xdd2e44)) embed.add_field(name='Details:', value=error) embed.set_footer(text="The bot owners have been notified of this error.") message_reference = await send_function(content='', embed=embed) self.last_traceback = traceback.format_exc() plugins.broadcast_event(self, 'bot_on_uncaught_exception', error, message) logger.error(self.last_traceback) logger.error(self.last_exception) if context: parsed_input = '[{0.subcommand}, {0.options}, {0.arguments}]'.format(context) else: parsed_input = '!Context is missing!' self.error_deque.appendleft(( context or message, parsed_input, self.last_exception, self.last_traceback)) await utilities.notify_owners( self, '```\n{0}\n{1}\n{2}\n{3}```'.format( message.content, parsed_input, self.last_exception, self.last_traceback)) return edit if edit else message_reference
async def handle_response( self, message, response, message_reference=None, replacement_message=None, context=None): """Handles responses. Arguments: message -- User message (can be bot sent message if no user message was used to handle the response). response -- commands.Response object. Keyword arguments: message_reference -- The message that the bot sent in response to the user message. replacement_message -- The old message_reference that was replaced. context -- A built bot.Context object. Response tuples contain a MessageTypes value. These specify what kind of behavior results from sending the message. See the commands module for documentation. """ # Respond because the bot hasn't yet if message_reference: response.message = message_reference else: message_reference = await self.respond( message, context, response, replacement_message=replacement_message) # Build partial context if no context is given if context is None: data_message = message_reference or message context = self.Context( message, None, None, None, None, None, None, Elevation.ALL, data_message.guild, data_message.channel, data_message.author, isinstance(data_message, PrivateChannel), None, None, self) # Change behavior based on message type if response.message_type is MessageTypes.NORMAL and message_reference: # Edited commands are handled in base.py wait_time = self.edit_timeout if wait_time: self.edit_dictionary[message.id] = message_reference await asyncio.sleep(wait_time) if message.id in self.edit_dictionary: del self.edit_dictionary[message.id] # Check for bot error - remove footer notification if message_reference.embeds: embed = message_reference.embeds[0] if embed.footer.text and embed.footer.text.startswith('\u200b' * 3): embed.set_footer() try: await message_reference.edit(embed=embed) except: pass elif response.message_type is MessageTypes.REPLACE: try: if self.selfbot and not replacement_message and message: # Edit instead send_arguments = response.get_send_kwargs(replacement_message) await message.edit(**send_arguments) else: if response.extra: await asyncio.sleep(response.extra) try: if message: await message.delete() if replacement_message: await message_reference.delete() except: # Ignore permissions errors pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.ACTIVE and message_reference: try: await response.extra_function(self, context, response) except Exception as e: # General error message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.INTERACTIVE and message_reference: # Get permissions if message_reference and isinstance(message_reference.channel, PrivateChannel): permissions = self.user.permissions_in(message_reference.channel) elif message_reference: permissions = message_reference.guild.me.permissions_in( message_reference.channel) else: permissions = None try: buttons = response.extra['buttons'] kwargs = response.extra.get('kwargs', {}) use_raw = response.extra.get('raw', False) events = ['reaction_add', 'reaction_remove'] if use_raw: events = [('raw_' + it) for it in events] if 'timeout' not in kwargs: kwargs['timeout'] = 300 if 'check' not in kwargs: if use_raw: kwargs['check'] = ( lambda p: ( p.message_id == message_reference.id and not data.get_member(self, p.user_id, attribute='bot', safe=True))) else: kwargs['check'] = ( lambda r, u: r.message.id == message_reference.id and not u.bot) level = context.subcommand.elevated_level if context.subcommand else None level = response.extra.get('elevation', level or Elevation.ALL) for button in buttons: await message_reference.add_reaction(button) # Ensure reactions are valid channel = message_reference.channel reaction_check = await channel.fetch_message(message_reference.id) for reaction in reaction_check.reactions: if not reaction.me or reaction.count > 1: async for user in reaction.users(): if user != self.user and permissions.manage_messages: asyncio.ensure_future( message_reference.remove_reaction(reaction, user)) # Notify plugin that reactions have been added await response.extra_function(self, context, response, None, False) # Read loop process_result = True while process_result is not False: try: # Read reaction additions (or removals) if permissions.manage_messages: # Can remove reactions result = await self.wait_for(events[0], **kwargs) if use_raw: if result.user_id != self.user.id: member = data.get_member( self, result.user_id, guild=message_reference.guild) asyncio.ensure_future( message_reference.remove_reaction( result.emoji, member)) else: continue pass else: if result[1] != self.user: asyncio.ensure_future( message_reference.remove_reaction(*result)) else: continue else: # Cannot remove reactions add_task = self.wait_for(events[0], **kwargs) remove_task = self.wait_for(events[1], **kwargs) done, pending = await asyncio.wait( [add_task, remove_task], return_when=FIRST_COMPLETED) result = next(iter(done)).result() for future in pending: future.cancel() # Check reaction validity if use_raw: member = data.get_member( self, result.user_id, guild=message_reference.guild) user_elevation = data.get_elevation(self, member=member) is_mod = user_elevation > Elevation.ALL # User cannot interact if not await utilities.can_interact( self, member, channel_id=message.channel.id): continue # Custom reactions disabled button_strings = [str(it) for it in buttons] if (response.extra.get('reactionlock', True) and str(result.emoji) not in button_strings): continue # User lock check if (response.extra.get('userlock', True) and not (member == message.author or is_mod)): continue # Command permissions check if user_elevation < level: continue else: user_elevation = data.get_elevation(self, member=result[1]) is_mod = user_elevation > Elevation.ALL # User cannot interact if not await utilities.can_interact( self, result[1], channel_id=message.channel.id): continue # Custom reactions disabled if response.extra.get('reactionlock', True) and not result[0].me: continue # User lock check if (response.extra.get('userlock', True) and not (result[1] == message.author or is_mod)): continue # Command permissions check if user_elevation < level: continue except (asyncio.futures.TimeoutError, asyncio.TimeoutError): # Notify plugin that the menu timed out await response.extra_function(self, context, response, None, True) process_result = False else: # Notify plugin that a valid reaction was read process_result = await response.extra_function( self, context, response, result, False) # Clear reactions after timeout try: await response.message.clear_reactions() except: # Ignore permissions errors (likely in DMs) pass autodelete = response.extra.get('autodelete', 0) if autodelete: await asyncio.sleep(autodelete) for it in (message_reference, message): try: await it.delete() except: pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.WAIT: try: kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 process_result = True while process_result is not False: try: result = await self.wait_for(response.extra['event'], **kwargs) except asyncio.TimeoutError: await response.extra_function(self, context, response, None) process_result = False else: process_result = await response.extra_function( self, context, response, result) if not response.extra.get('loop', False): process_result = False autodelete = response.extra.get('autodelete', 0) if autodelete: await asyncio.sleep(autodelete) for it in (message_reference, message): try: await it.delete() except: pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif message_reference: logger.error("Unknown message type: %s", response.message_type)
async def on_message(self, message, replacement_message=None): # Ensure bot can respond properly try: initial_data = self.can_respond(message) except Exception as e: # General error logger.error(e) logger.error(traceback.format_exc()) self.last_exception = e return if not initial_data: return # Ensure command is valid content = initial_data[0] elevation = Elevation.BOT_OWNERS - (initial_data[4:0:-1] + [True]).index(True) split_content = content.split(' ', 1) if len(split_content) == 1: # No spaces split_content.append('') base, parameters = split_content base = base.lower() try: command = self.commands[base] except KeyError: if self.single_command: try: parameters = content base = self.single_command command = self.commands[base] except KeyError: logger.error("Single command fill not found!") return else: logger.debug("Suitable command not found: %s", base) return # Check that user is not spamming author_id = message.author.id direct = isinstance(message.channel, PrivateChannel) spam_value = self.spam_dictionary.get(author_id, 0) if elevation > Elevation.ALL or direct: # Moderators ignore custom limit spam_limit = self.spam_limit else: spam_limit = min( self.spam_limit, data.get( self, 'core', 'spam_limit', guild_id=message.guild.id, default=self.spam_limit)) if spam_value >= spam_limit: if spam_value == spam_limit: self.spam_dictionary[author_id] = spam_limit + 1 plugins.broadcast_event(self, 'bot_on_user_ratelimit', message.author) await message.channel.send(content=( "{0}, you appear to be issuing/editing " "commands too quickly. Please wait {1} seconds.".format( message.author.mention, self.spam_timeout))) return context = None try: # Check for maintenance mode if self.maintenance_mode and elevation != Elevation.BOT_OWNERS: if self.maintenance_mode == 1: # Ignore attempts to respond if not 1 if self.maintenance_message: fields = [('\u200b', self.maintenance_message)] else: fields = [] raise CBException( "The bot is currently in maintenance mode.", embed_fields=fields, editable=False) else: return # Parse command and reply logger.debug('%s (%s): %s', message.author, message.author.id, message.content) parse_command = self._parse_command( message, command, parameters, initial_data, elevation, direct) if replacement_message: context = await parse_command response = await self._get_response(context) else: with message.channel.typing(): context = await parse_command response = await self._get_response(context) self.response_deque.appendleft((context, response)) except Exception as e: # General error response = Response() message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) else: # Attempt to respond message_reference = await self.respond( message, context, response, replacement_message=replacement_message) # Incremement the spam dictionary entry if author_id in self.spam_dictionary: self.spam_dictionary[author_id] += 1 else: self.spam_dictionary[author_id] = 1 self.last_response = message_reference self.last_context = context await self.handle_response( message, response, message_reference=message_reference, replacement_message=replacement_message, context=context)
def start(start_file=None, debug=False): if start_file: path = os.path.split(os.path.realpath(start_file))[0] logger.debug("Setting directory to " + path) else: # Use Docker setup path = '/external' logger.info("Bot running in Docker mode.") logger.debug("Using Docker setup path, " + path) try: config_file_location = path + '/config/core-config.yaml' with open(config_file_location, 'rb') as config_file: config = yaml.load(config_file) selfbot_mode, token = config['selfbot_mode'], config['token'] except Exception as e: logger.error("Could not determine token /or selfbot mode.") raise e if selfbot_mode: client_type = discord.Client logger.debug("Using standard client (selfbot enabled).") else: client_type = discord.AutoShardedClient logger.debug("Using autosharded client (selfbot disabled).") if debug: log_file = '{}/temp/logs.txt'.format(path) if os.path.isfile(log_file): shutil.copy2(log_file, '{}/temp/last_logs.txt'.format(path)) logging.basicConfig(level=logging.DEBUG, handlers=[ RotatingFileHandler(log_file, maxBytes=1000000, backupCount=3), logging.StreamHandler() ]) def safe_exit(): loop = asyncio.get_event_loop() try: # From discord.py client.run loop.run_until_complete(bot.logout()) pending = asyncio.Task.all_tasks() gathered = asyncio.gather(*pending) except Exception as e: logger.error("Failed to log out. %s", e) try: gathered.cancel() loop.run_until_complete(gathered) gathered.exception() except: pass logger.warn("Bot disconnected. Shutting down...") bot.shutdown() # Calls sys.exit def exception_handler(loop, context): e = context.get('exception') if e and e.__traceback__: traceback_text = ''.join(traceback.format_tb(e.__traceback__)) else: traceback_text = traceback.format_exc() if not traceback_text: traceback_text = '(No traceback available)' error_message = '{}\n{}'.format(e, traceback_text) logger.error("An uncaught exception occurred.\n" + error_message) with open(path + '/temp/error.txt', 'w') as error_file: error_file.write(error_message) logger.error("Error file written.") if bot.is_closed(): safe_exit() loop = asyncio.get_event_loop() bot = get_new_bot(client_type, path, debug) start_task = bot.start(token, bot=not selfbot_mode) loop.set_exception_handler(exception_handler) try: loop.run_until_complete(start_task) except KeyboardInterrupt: logger.warn("Interrupted!") safe_exit()
async def handle_error(self, error, message, context, response, edit=None, command_editable=False): """Common error handler for sending responses.""" send_function = edit.edit if edit else message.channel.send self.last_exception = error if response.message: try: await response.message.clear_reactions() except: pass if isinstance(error, BotException): self.last_traceback = error.traceback plugins.broadcast_event(self, 'bot_on_error', error, message) if error.use_embed: content, embed = '', error.embed else: content, embed = str(error), None if command_editable: if content: content += '\n\n(Note: The issuing command can be edited)' embed.set_footer( text= "\u200b\u200b\u200bThe issuing command can be edited", icon_url="http://i.imgur.com/fM9yGzI.png") message_reference = await send_function(content=content, embed=embed) if error.autodelete > 0: await asyncio.sleep(error.autodelete) try: # Avoid delete_messages for selfbot mode message_reference = edit if edit else message_reference await self.delete_message(message_reference, reason='Automatic') await self.delete_message(message, reason='Automatic') except: pass return elif isinstance(error, discord.Forbidden): plugins.broadcast_event(self, 'bot_on_discord_error', error, message) message_reference = None try: await message.author.send( content="Sorry, I don't have permission to carry " "out that command in that channel. The bot may have had " "its `Send Messages` permission revoked (or any other " "necessary permissions, like `Manage Messages` or " "`Speak`).\nIf you are a bot moderator or server owner, " "you can mute channels with `{}mod mute <channel>` " "instead of using permissions directly. If that's not the " "issue, be sure to check that the bot has the proper " "permissions on the server and each channel!".format( self.command_invokers[0])) except: # User has blocked the bot pass else: if isinstance( error, discord.HTTPException) and len(str(response)) > 1998: plugins.broadcast_event(self, 'bot_on_discord_error', error, message) message_reference = await utilities.send_text_as_file( message.channel, str(response), 'response', extra= "The response is too long. Here is a text file of the contents." ) else: insult = random.choice(exception_insults) error = '**`{0}:`**`{1}`'.format( type(error).__name__, error) embed = discord.Embed(title=':x: Internal error', description=insult, colour=discord.Colour(0xdd2e44)) embed.add_field(name='Details:', value=error) embed.set_footer( text="The bot owners have been notified of this error." ) message_reference = await send_function(content='', embed=embed) self.last_traceback = traceback.format_exc() plugins.broadcast_event(self, 'bot_on_general_error', error, message) logger.error(self.last_traceback) logger.error(self.last_exception) parsed_input = '[{0.subcommand}, {0.options}, {0.arguments}]'.format( context) await utilities.notify_owners( self, '```\n{0}\n{1}\n{2}\n{3}```'.format( message.content, parsed_input, self.last_exception, self.last_traceback)) return edit if edit else message_reference
async def on_message(self, message, replacement_message=None): # Ensure bot can respond properly try: initial_data = self.can_respond(message) except Exception as e: # General error logger.error(e) logger.error(traceback.format_exc()) self.last_exception = e return if not initial_data: return # Ensure command is valid content = initial_data[0] elevation = 3 - (initial_data[4:0:-1] + [True]).index(True) split_content = content.split(' ', 1) if len(split_content) == 1: # No spaces split_content.append('') base, parameters = split_content base = base.lower() try: command = self.commands[base] except KeyError: logger.debug("Suitable command not found: " + base) return # Check that user is not spamming author_id = message.author.id direct = isinstance(message.channel, PrivateChannel) spam_value = self.spam_dictionary.get(author_id, 0) if elevation > 0 or direct: # Moderators ignore custom limit spam_limit = self.spam_limit else: spam_limit = min( self.spam_limit, data.get(self, 'core', 'spam_limit', guild_id=message.guild.id, default=self.spam_limit)) if spam_value >= spam_limit: if spam_value == spam_limit: self.spam_dictionary[author_id] = spam_limit + 1 plugins.broadcast_event(self, 'bot_on_user_ratelimit', message.author) await self.send_message( message.channel, "{0}, you appear to be issuing/editing " "commands too quickly. Please wait {1} seconds.". format(message.author.mention, self.spam_timeout)) return # Parse command and reply try: context = None with message.channel.typing(): logger.debug(message.author.name + ': ' + message.content) subcommand, options, arguments = parser.parse( self, command, parameters, message) context = self.Context( message, base, subcommand, options, arguments, subcommand.command.keywords, initial_data[0], elevation, message.guild, message.channel, message.author, direct, subcommand.index, subcommand.id, self) plugins.broadcast_event(self, 'bot_on_command', context) logger.info([subcommand, options, arguments]) response = await commands.execute(self, context) if response is None: response = Response() if self.selfbot and response.content: response.content = '\u200b' + response.content except Exception as e: # General error response = Response() destination = message.channel message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) else: # Attempt to respond send_arguments = response.get_send_kwargs(replacement_message) try: destination = response.destination if response.destination else message.channel message_reference = None if replacement_message: try: await replacement_message.edit(**send_arguments) message_reference = replacement_message except discord.NotFound: # Message deleted response = Response() message_reference = None elif (not response.is_empty() and not (self.selfbot and response.message_type is MessageTypes.REPLACE)): message_reference = await destination.send( **send_arguments) response.message = message_reference plugins.broadcast_event(self, 'bot_on_response', response, context) except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) # Incremement the spam dictionary entry if author_id in self.spam_dictionary: self.spam_dictionary[author_id] += 1 else: self.spam_dictionary[author_id] = 1 # MessageTypes: # NORMAL - Normal. The issuing command can be edited. # PERMANENT - Message is not added to the edit dictionary. # REPLACE - Deletes the issuing command after 'extra' seconds. Defaults # to 0 seconds if 'extra' is not given. # ACTIVE - The message reference is passed back to the function defined # with 'extra_function'. If 'extra_function' is not defined, it will call # plugin.handle_active_message. # INTERACTIVE - Assembles reaction buttons given by extra['buttons'] and # calls 'extra_function' whenever one is pressed. # WAIT - Wait for event. Calls 'extra_function' with the result, or None # if the wait timed out. # # Only the NORMAL message type can be edited. response.message = message_reference if message_reference and isinstance(message_reference.channel, PrivateChannel): permissions = self.user.permissions_in( message_reference.channel) elif message_reference: permissions = message_reference.guild.me.permissions_in( message_reference.channel) else: permissions = None self.last_response = message_reference if response.message_type is MessageTypes.NORMAL: # Edited commands are handled in base.py if message_reference is None: # Forbidden exception return wait_time = self.edit_timeout if wait_time: self.edit_dictionary[str(message.id)] = message_reference await asyncio.sleep(wait_time) if str(message.id) in self.edit_dictionary: del self.edit_dictionary[str(message.id)] if message_reference.embeds: embed = message_reference.embeds[0] if embed.footer.text and embed.footer.text.startswith( '\u200b' * 3): embed.set_footer() try: await message_reference.edit(embed=embed) except: pass elif response.message_type is MessageTypes.REPLACE: try: if self.selfbot and not replacement_message: # Edit instead await message.edit(**send_arguments) else: if response.extra: await asyncio.sleep(response.extra) try: await message.delete(reason='Automatic') except: # Ignore permissions errors pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.ACTIVE: if message_reference is None: # Forbidden exception return try: await response.extra_function(self, context, response) except Exception as e: # General error message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.INTERACTIVE: try: buttons = response.extra['buttons'] kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 if 'check' not in kwargs: kwargs['check'] = (lambda r, u: r.message.id == message_reference.id and not u.bot) for button in buttons: await message_reference.add_reaction(button) reaction_check = await destination.get_message( message_reference.id) for reaction in reaction_check.reactions: if not reaction.me or reaction.count > 1: async for user in reaction.users(): if user != self.user and permissions.manage_messages: asyncio.ensure_future( message_reference.remove_reaction( reaction, user)) await response.extra_function(self, context, response, None, False) process_result = True while process_result is not False: try: if not permissions.manage_messages: add_task = self.wait_for( 'reaction_add', **kwargs) remove_task = self.wait_for( 'reaction_remove', **kwargs) done, pending = await asyncio.wait( [add_task, remove_task], return_when=FIRST_COMPLETED) result = next(iter(done)).result() for future in pending: future.cancel() else: # Can remove reactions result = await self.wait_for( 'reaction_add', **kwargs) if result[1] != self.user: asyncio.ensure_future( message_reference.remove_reaction( *result)) else: continue is_mod = data.is_mod(self, message.guild, result[1].id) if (response.extra.get('reactionlock', True) and not result[0].me or (response.extra.get('userlock', True) and not (result[1] == message.author or is_mod))): continue except (asyncio.futures.TimeoutError, asyncio.TimeoutError): await response.extra_function( self, context, response, None, True) process_result = False else: process_result = await response.extra_function( self, context, response, result, False) try: await response.message.clear_reactions() except: pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.WAIT: try: kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 process_result = True while process_result is not False: try: result = await self.wait_for( response.extra['event'], **kwargs) except asyncio.TimeoutError: await response.extra_function( self, context, response, None) process_result = False else: process_result = await response.extra_function( self, context, response, result) if not response.extra.get('loop', False): process_result = False except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference else: logger.error("Unknown message type: {}".format( response.message_type)) '''
def __init__( self, error_subject, error_details, *args, e=None, error_type=ErrorTypes.RECOVERABLE, edit_message=None, autodelete=0, use_embed=True, embed_fields=[], embed_format={}, serious=False, editable=True): """ Arguments: error_subject -- The error title. Generally the plugin name. error_details -- Primary error message. args -- A list of additional errors that get appended after the details. Keyword arguments: e -- The provided exception object itself. error_type -- Determines if the bot can recover from the exception. edit_message -- Edits the given message with the exception text or embed. autodelete -- Deletes after the given number of seconds, unless it is 0. use_embed -- The error should be displayed as an embed. embed_fields -- Additional fields used for providing titled descriptions of the error. embed_format -- Used to format the strings of the values in the embed fields. serious -- If True, always uses the :warning: emoji. editable -- Whether or not the error displays an "issuing command is editable" note. """ self.error_type = error_type self.error_subject = str(error_subject) self.error_details = str(error_details) self.error_other = args self.provided_exception = e self.autodelete = 0 if autodelete is None else autodelete self.use_embed = use_embed self.editable = editable self.traceback = '' self.other_details = '\n'.join([str(arg) for arg in self.error_other]) self.error_message = "`{subject} error: {details}`\n{others}".format( subject=self.error_subject, details=self.error_details, others=self.other_details) emoji = ':warning:' if serious or random() > 0.01 else ':thinking:' self.embed = Embed( title='{} {} error'.format(emoji, self.error_subject), description='{}\n{}'.format(self.error_details, self.other_details), colour=Colour(0xffcc4d)) if self.provided_exception: if isinstance(self.provided_exception, BotException): given_error = '{}'.format(self.provided_exception.error_details) embed_fields = self.provided_exception.embed_fields else: given_error = '`{}: {}`'.format( type(self.provided_exception).__name__, self.provided_exception) self.error_message += '\nGiven error:\n{}'.format(given_error) embed_fields = [('Given error:', given_error)] + embed_fields if self.provided_exception.__traceback__: self.traceback = traceback.format_tb(self.provided_exception.__traceback__) else: self.traceback = traceback.format_exc() self.embed_fields = embed_fields for name, value in embed_fields: self.embed.add_field(name=name, value=value.format(**embed_format), inline=False) logger.error(self.error_message) # If non-recoverable, quit if error_type in (ErrorTypes.STARTUP, ErrorTypes.FATAL, ErrorTypes.INTERNAL): traceback.print_exc() sys.exit() # Edit the given message with the error if edit_message: content, embed = ('', self.embed) if self.use_embed else (str(self), None) asyncio.ensure_future(edit_message.edit(content=content, embed=embed))