Пример #1
0
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)
Пример #2
0
    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))
Пример #3
0
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())
Пример #4
0
 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()
Пример #5
0
 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()
Пример #6
0
 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
Пример #7
0
 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
Пример #8
0
 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)
Пример #9
0
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())
Пример #10
0
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)
Пример #11
0
        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
Пример #12
0
        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)
Пример #13
0
        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)
Пример #14
0
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()
Пример #15
0
        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
Пример #16
0
        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))
            '''
Пример #17
0
    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))