Ejemplo n.º 1
0
def _update_current_game(bot, safe=False, include_setup_status=False):
    """Updates the index of the latest/current game."""
    schedule_data = data.get(bot, __name__, 'schedule', volatile=True, default=[])
    current_time = datetime.datetime.utcnow()
    for index, game in enumerate(schedule_data):
        start_time, end_time = game['scheduled'], game['end']
        if start_time <= current_time < end_time:  # Update latest index
            data.add(bot, __name__, 'current_index', index, volatile=True)
            data.add(bot, __name__, 'current_game', game, volatile=True)
            if include_setup_status:
                setup_time = datetime.timedelta(seconds=game['setup_seconds'])
                return index, (current_time < start_time + setup_time)
            else:
                return index
        elif current_time < start_time:
            logger.debug("The current time is less than the start time. Index: %s", index)
            break
    else:  # GDQ over, or past schedule
        game, index = None, 999
    if safe:
        data.add(bot, __name__, 'current_index', index, volatile=True)
        data.add(bot, __name__, 'current_game', game, volatile=True)
        if include_setup_status:
            return index, True
        else:
            return index
    raise CBException("No current game was found.")
Ejemplo n.º 2
0
async def member_banned(bot, guild, user):
    """Dumps logs when a user is banned."""
    try:
        _check_log_channel(bot, guild)
    except BotException:
        return

    # Ensure audit logs have been updated and pull ban information
    await asyncio.sleep(3)
    moderator_id = None
    details = '{0} (<@{0.id}>) was banned'.format(user)
    async for entry in guild.audit_logs(limit=50,
                                        action=discord.AuditLogAction.ban):
        if entry.target == user:
            details += ' by {0} (<@{0.id}>): {1}'.format(
                entry.user, entry.reason or 'No reason provided')
            moderator_id = entry.user.id
            break
    logger.debug("Details: %s", details)

    # Show confirmation menu for logging
    await automated_dump_message(bot,
                                 guild,
                                 details,
                                 query=user.id,
                                 moderator_id=moderator_id)
Ejemplo n.º 3
0
def load_data(bot):
    """Loads the data from the data directory."""

    logger.debug("Loading data...")
    directory = bot.path + '/data/'
    for guild in bot.guilds:
        guild_id = str(guild.id)
        try:
            with open(directory + guild_id + '.json', 'r') as guild_file:
                bot.data[guild_id] = json.load(guild_file)
        except:
            logger.warn("Data for guild {} not found.".format(guild_id))
            bot.data[guild_id] = {}

    try:
        with open(directory + 'global_plugins.json', 'r') as plugins_file:
            bot.data['global_plugins'] = json.load(plugins_file)
    except:
        logger.warn("Global data for plugins not found.")
    try:
        with open(directory + 'global_users.json', 'r') as users_file:
            bot.data['global_users'] = json.load(users_file)
    except:
        logger.warn("Global data for users not found.")
    logger.debug("Data loaded.")
Ejemplo n.º 4
0
def db_backup(bot, safe=True):
    """Use the Docker setup to backup the database."""
    if not bot.docker_mode:
        return
    try:
        logger.debug("Attemping to connect to the database container...")
        if bot.dump_exclusions:
            exclusions = '-T "' + '" -T "'.join(bot.dump_exclusions) + '"'
        else:
            exclusions = ''
        command = (
            'pg_dump -U postgres -F c {} postgres > '
            '/external/data/db_dump'.format(exclusions))
        docker_send_command(command)
        logger.debug("Told database container to backup")
    except Exception as e:
        logger.warn("Failed to communicate with the database container: %s", e)
        if safe:
            return
        raise CBException("Failed to communicate with the database container.", e=e)

    # Read response code from database container
    try:
        return docker_receive_exit_code()
    except Exception as e:
        logger.warn("Failed to receive a response from the database container: %s", e)
        if safe:
            return
        raise CBException("Failed to receive a response from the database container.", e=e)
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
def save_data(bot, force=False):
    """Saves all of the current data in the data dictionary.

    Does not save volatile_data, though. Backups 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:
                    json.dump(value, current_file, indent=4)
            # 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)
Ejemplo n.º 7
0
def load_data(bot):
    """Loads the data from the data directory."""

    logger.debug("Loading data...")
    directory = bot.path + '/data/'
    for guild in bot.guilds:
        guild_id = str(guild.id)
        try:
            with open(directory + guild_id + '.json', 'r') as guild_file:
                bot.data[guild_id] = json.load(guild_file)
        except:
            logger.warn("Data for guild {} not found.".format(guild_id))
            bot.data[guild_id] = {}

    try:
        with open(directory + 'global_plugins.json', 'r') as plugins_file:
            bot.data['global_plugins'] = json.load(plugins_file)
    except:
        logger.warn("Global data for plugins not found.")
    try:
        with open(directory + 'global_users.json', 'r') as users_file:
            bot.data['global_users'] = json.load(users_file)
    except:
        logger.warn("Global data for users not found.")
    logger.debug("Data loaded.")
Ejemplo n.º 8
0
def _update_current_game(bot, safe=False, include_setup_status=False):
    """Updates the index of the latest/current game."""
    schedule_data = data.get(bot,
                             __name__,
                             'schedule',
                             volatile=True,
                             default=[])
    current_time = datetime.datetime.utcnow()
    for index, game in enumerate(schedule_data):
        start_time, end_time = game['scheduled'], game['end']
        if start_time <= current_time < end_time:  # Update latest index
            data.add(bot, __name__, 'current_index', index, volatile=True)
            data.add(bot, __name__, 'current_game', game, volatile=True)
            if include_setup_status:
                setup_time = datetime.timedelta(seconds=game['setup_seconds'])
                return index, (current_time < start_time + setup_time)
            else:
                return index
        elif current_time < start_time:
            logger.debug(
                "The current time is less than the start time. Index: %s",
                index)
            break
    else:  # GDQ over, or past schedule
        game, index = None, 999
    if safe:
        data.add(bot, __name__, 'current_index', index, volatile=True)
        data.add(bot, __name__, 'current_game', game, volatile=True)
        if include_setup_status:
            return index, True
        else:
            return index
    raise CBException("No current game was found.")
Ejemplo n.º 9
0
def add_manual(bot, clean_name, plugin_name):
    """Reads all manuals in the config folder and adds them to the bot."""
    directory = bot.path + '/config/'
    try:
        with open(directory + clean_name + '-manual.yaml',
                  'rb') as manual_file:
            raw_manual = yaml.load(manual_file)
    except FileNotFoundError:
        logger.debug("No manual found for {}.".format(plugin_name))
        return
    except yaml.YAMLError as e:  # TODO: Change
        raise CBException(
            "Failed to parse the manual for {}.".format(plugin_name), e=e)
    try:
        for subject, topics in raw_manual.items():
            assert len(topics)
            for topic_group in topics:
                assert len(topic_group) >= 2
            bot.manuals.update(
                {subject.lower(): {
                     'subject': subject,
                     'topics': topics
                 }})
    except:
        raise CBException(
            "Manual for {} is improperly structured.".format(plugin_name))
Ejemplo n.º 10
0
async def _session_timeout_notify(
        bot, scheduled_time, payload, search, destination, late, info, id, *args):
    logger.debug("Notifying a session timeout.")
    clear_result = await _clear_webhook(bot, search)
    if clear_result:
        messageable = utilities.get_messageable(bot, destination)
        await messageable.send(content="Your character creation/editing session has timed out.")
Ejemplo n.º 11
0
def docker_send_command(command):
    """Sends the database Docker container a command."""
    logger.debug("Sending database container command: %s", command)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(0.1)
    s.connect(('db', 2345))
    s.send(bytes(command, 'ascii'))
    s.close()
Ejemplo n.º 12
0
Archivo: core.py Proyecto: sasma/JshBot
 def shutdown(self):
     logger.debug("Writing data on shutdown...")
     if self.fresh_boot is not None:  # Don't write blank data
         self.save_data(force=True)
     logger.info("Closing down!")
     try:
         self.loop.close()
     except:
         pass
     sys.exit()
Ejemplo n.º 13
0
 async def _leave():
     await asyncio.sleep(delay)
     test_voice_client = guild.voice_client
     if not test_voice_client or test_voice_client.source != audio_source:
         logger.debug("Voice client changed. Automatic disconnect cancelled.")
     else:
         try:
             await voice_client.disconnect()
         except Exception as e:
             raise CBException("Failed to disconnect from the voice channel.", e=e)
Ejemplo n.º 14
0
def add_plugins(bot):
    """
    Gets a list of all of the plugins and stores them as a key/value pair of
    the plugin name and the module itself (renamed to plugin for the user).
    In addition, this also sets the commands given by each plugin.
    """
    directory = '{}/plugins'.format(bot.path)
    data_directory = '{}/plugins/plugin_data'.format(bot.path)
    if os.path.isdir(data_directory):
        sys.path.append(data_directory)
    try:
        plugins_list = os.listdir(directory)
    except FileNotFoundError:
        raise CBException("Plugins directory not found", error_type=ErrorTypes.STARTUP)

    # Add base plugin
    # Order is always add: plugin, configuration, manual, commands
    from jshbot import base
    bot.plugins['core'] = base
    add_manual(bot, 'core', 'core')
    while command_spawner_functions:
        function = command_spawner_functions.pop()
        add_commands(bot, function(bot), base)
    while db_template_functions:
        function = db_template_functions.pop()
        template = function(bot)
        for name, value in template.items():
            data.db_add_template(bot, name, value)
    while command_load_functions:
        function = command_load_functions.pop()
        function(bot)
    while event_functions:
        event_name, function = event_functions.pop()
        if event_name not in bot.event_functions:
            bot.event_functions[event_name] = [function]
        else:
            bot.event_functions[event_name].append(function)
    while plugin_permissions:
        function = plugin_permissions.pop()
        utilities.add_bot_permissions(bot, 'core', **function(bot))

    # Add plugins in plugin folder
    for plugin_name in plugins_list:
        if (plugin_name[0] in ('.', '_') or
                plugin_name == 'base' or
                not plugin_name.endswith('.py')):
            continue
        try:
            load_plugin(bot, plugin_name)
        except Exception as e:
            raise CBException(
                "Failed to import external plugin on startup.", e=e, error_type=ErrorTypes.STARTUP)

    if len(bot.plugins) - 1:
        logger.debug("Loaded %s plugin(s)", len(bot.plugins) - 1)
Ejemplo n.º 15
0
 def shutdown(self):
     logger.debug("Writing data on shutdown...")
     if self.fresh_boot is not None:  # Don't write blank data
         self.save_data(force=True)
     logger.info("Closing down!")
     try:
         self.loop.close()
     except:
         pass
     logging.shutdown()
     sys.exit()
Ejemplo n.º 16
0
def docker_receive_exit_code():
    """Waits until an exit code is returned from the database Docker container."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(30)
    s.bind(('', 2345))
    s.listen(1)
    connection, _ = s.accept()
    response = connection.recv(64)
    connection.close()
    s.close()
    logger.debug("Database container response: %s", response)
    return response
Ejemplo n.º 17
0
Archivo: core.py Proyecto: sasma/JshBot
        def __init__(self, path, debug):
            self.version = '0.4.0-rewrite'
            self.date = 'July 3rd, 2017'
            self.time = int(time.time())
            self.readable_time = time.strftime('%c')
            self.path = path
            self.debug = debug

            logger.info("=== {0: ^40} ===".format("Starting up JshBot " +
                                                  self.version))
            logger.info("=== {0: ^40} ===".format(self.readable_time))

            super().__init__()

            self.configurations = {}
            plugins.add_configuration(self, 'core', 'core', base)
            data.check_folders(self)

            logger.debug("Connecting to database...")
            self.db_templates = {}
            self.db_connection = None
            data.db_connect(self)

            logger.debug("Loading plugins...")
            self.data = {'global_users': {}, 'global_plugins': {}}
            self.volatile_data = {'global_users': {}, 'global_plugins': {}}
            self.data_changed = []
            self.plugins = OrderedDict()
            self.manuals = OrderedDict()
            self.commands = {}
            plugins.add_plugins(self)

            config = self.configurations['core']
            self.edit_dictionary = {}
            self.spam_dictionary = {}
            self.spam_limit = config['command_limit']
            self.spam_timeout = config['command_limit_timeout']
            self.command_invokers = config['command_invokers']
            self.locked_commands = config['locked_commands']
            self.edit_timeout = config['edit_timeout']
            self.selfbot = config['selfbot_mode']
            self.owners = config['owners']
            self.schedule_timer = None
            self.last_exception = None
            self.last_traceback = None
            self.last_response = None
            self.fresh_boot = None
            self.extra = None

            # Extras
            config['token'] = '(redacted)'
            config['database_credentials'] = '(redacted)'
Ejemplo n.º 18
0
async def setup_globals(bot):
    """Sets up the DATA_CHANNEL global"""
    global DATA_CHANNEL
    DATA_CHANNEL = data.get_channel(bot, configurations.get(bot, __name__, key='data_channel'))
    if not DATA_CHANNEL:
        logger.warn("Failed to find the data channel. Defaulting to the upload channel.")
        DATA_CHANNEL = data.get_channel(bot, configurations.get(bot, 'core', key='upload_channel'))

    # Clear any webhooks (debug)
    webhooks = await DATA_CHANNEL.webhooks()
    for webhook in webhooks:
        logger.debug("Deleting webhook %s", webhook)
        await webhook.delete()
Ejemplo n.º 19
0
async def _cycle_specific(bot, cycle_type):
    table_name = 'txyz_' + TYPE_NAMES[cycle_type]
    cursor = data.db_select(bot, from_arg=table_name, additional='ORDER BY RANDOM()', limit=1)
    result = cursor.fetchone()
    if result:
        result = result[1]
    else:
        result = DEFAULTS[cycle_type]
    txyz_guild = bot.get_guild(TXYZ_GUILD)
    selected_channel = txyz_guild.voice_channels[cycle_type]
    await selected_channel.edit(name='_' + result)
    logger.debug('New %s: %s', table_name[5:-1], result)
    return result
Ejemplo n.º 20
0
async def _clear_webhook(bot, webhook_id):
    """Clears the webhook from volatile data."""
    logger.debug("Removing webhook: %s", webhook_id)
    utilities.remove_schedule_entries(bot, __name__, search=str(webhook_id))
    owner = data.remove(bot, __name__, 'owner', user_id=webhook_id, safe=True, volatile=True)
    data.remove(bot, __name__, 'stage', user_id=webhook_id, safe=True, volatile=True)
    if not owner:
        return False
    webhook = data.remove(bot, __name__, 'tracker', user_id=owner.id, volatile=True)
    try:
        await webhook.delete()
        return True
    except Exception as e:
        logger.warn("Failed to delete webhook after data checking failure.")
        return False
Ejemplo n.º 21
0
async def _start_scheduler(bot):
    """Starts the interal scheduler."""
    await bot.wait_until_ready()
    if bot.schedule_timer:  # Scheduler already running
        bot.schedule_timer.cancel()
        bot.schedule_timer = None
    cursor = data.db_select(
        bot, from_arg='schedule', additional='ORDER BY time ASC', limit=1, safe=False)
    result = cursor.fetchone()
    if result:
        delta = result.time - time.time()
        logger.debug("Starting scheduled event %s", result.id)
        bot.schedule_timer = asyncio.ensure_future(_schedule_timer(bot, result, delta))
    else:
        logger.debug("No pending scheduled event available.")
Ejemplo n.º 22
0
def add_plugins(bot):
    """
    Gets a list of all of the plugins and stores them as a key/value pair of
    the plugin name and the module itself (renamed to plugin for the user).
    In addition, this also sets the commands given by each plugin.
    """
    directory = '{}/plugins'.format(bot.path)
    data_directory = '{}/plugins/plugin_data'.format(bot.path)
    if os.path.isdir(data_directory):
        logger.debug("Setting plugin_data as plugin import path.")
        sys.path.append(data_directory)
    try:
        plugins_list = os.listdir(directory)
    except FileNotFoundError:
        raise CBException("Plugins directory not found",
                          error_type=ErrorTypes.STARTUP)

    # Add base plugin
    # Order is always add: plugin, configuration, manual, commands
    from jshbot import base
    bot.plugins['core'] = base
    add_manual(bot, 'core', 'core')
    while command_spawner_functions:
        function = command_spawner_functions.pop()
        add_commands(bot, function(bot), base)
    while db_template_functions:
        function = db_template_functions.pop()
        template = function(bot)
        for name, value in template.items():
            data.db_add_template(bot, name, value)
    while command_load_functions:
        function = command_load_functions.pop()
        function(bot)

    # Add plugins in plugin folder
    for plugin_name in plugins_list:
        if (plugin_name[0] in ('.', '_') or plugin_name == 'base'
                or not plugin_name.endswith('.py')):
            continue
        try:
            load_plugin(bot, plugin_name)
        except Exception as e:
            raise CBException("Failed to import external plugin on startup.",
                              e=e,
                              error_type=ErrorTypes.STARTUP)

    if len(bot.plugins) - 1:
        logger.debug("Loaded {} plugin(s)".format(len(bot.plugins) - 1))
Ejemplo n.º 23
0
async def fill_shortcut(bot, shortcut, parameters, message):
    parameters = split_parameters(parameters, include_quotes=True)
    stripped_parameters = parameters[::2]
    arguments_dictionary = {}
    current_index = -1
    for current_index, current in enumerate(stripped_parameters):
        if current_index >= len(shortcut.args):
            invoker = utilities.get_invoker(bot, guild=message.guild)
            raise CBException(
                "Too many arguments.", embed_fields=shortcut.help_embed_fields,
                embed_format={'invoker': invoker})
        else:
            arg = shortcut.args[current_index]
            if arg.argtype in (ArgTypes.SINGLE, ArgTypes.OPTIONAL):
                arguments_dictionary[arg.name] = current
            else:  # Instant finish grouped arguments
                if arg.argtype in (ArgTypes.SPLIT, ArgTypes.SPLIT_OPTIONAL):
                    arguments_dictionary[arg.name] = ''.join(stripped_parameters[current_index:])
                else:  # Merged
                    arguments_dictionary[arg.name] = ''.join(parameters[current_index * 2:])
                break
    # TODO: TEST THIS!
    logger.debug("Finished shortcut loop. %s", arguments_dictionary)
    if current_index < len(shortcut.args) - 1:  # Check for optional arguments
        arg = shortcut.args[current_index + 1]
        if arg.argtype is ArgTypes.OPTIONAL:
            while (arg and arg.argtype is ArgTypes.OPTIONAL and
                    current_index < len(shortcut.args)):
                arguments_dictionary[arg.name] = '' if arg.default is None else arg.default
                current_index += 1
                try:
                    arg = shortcut.args[current_index]
                except:
                    arg = None
        if arg and arg.argtype in (ArgTypes.SPLIT_OPTIONAL, ArgTypes.MERGED_OPTIONAL):
            arguments_dictionary[arg.name] = '' if arg.default is None else arg.default
        elif arg:
            invoker = utilities.get_invoker(bot, guild=message.guild)
            raise CBException(
                "Not enough arguments.", embed_fields=shortcut.help_embed_fields,
                embed_format={'invoker': invoker})
    logger.debug("Finished checking for optional arguments. %s", arguments_dictionary)
    for arg in shortcut.args:
        value = arguments_dictionary[arg.name]
        if value is not None:
            new_value = await arg.convert_and_check(bot, message, value)
            arguments_dictionary[arg.name] = new_value
    return shortcut.replacement.format(**arguments_dictionary).strip()
Ejemplo n.º 24
0
async def _cycle_specific(bot, cycle_type):
    table_name = 'txyz_' + TYPE_NAMES[cycle_type]
    cursor = data.db_select(bot,
                            from_arg=table_name,
                            additional='ORDER BY RANDOM()',
                            limit=1)
    result = cursor.fetchone()
    if result:
        result = result[1]
    else:
        result = DEFAULTS[cycle_type]
    txyz_guild = bot.get_guild(TXYZ_GUILD)
    selected_channel = txyz_guild.voice_channels[cycle_type]
    await selected_channel.edit(name='_' + result)
    logger.debug('New %s: %s', table_name[5:-1], result)
    return result
Ejemplo n.º 25
0
async def parse(bot, command, parameters, message):
    """Parses the parameters and returns a tuple.

    This matches the parameters to a subcommand.
    The tuple is (base, subcommand_index, options, arguments).
    """
    parameters = parameters.strip()  # Safety strip

    if isinstance(command, commands.Shortcut):  # Fill replacement string
        logger.debug("Filling shortcut...")
        parameters = await fill_shortcut(bot, command, parameters, message)
        command = command.command  # command is actually a Shortcut. Not confusing at all
        logger.debug("Shortcut filled to: [%s]", parameters)

    subcommand, options, arguments = await match_subcommand(bot, command, parameters, message)

    return (subcommand, options, arguments)
Ejemplo n.º 26
0
def parse(bot, command, parameters, message):
    """Parses the parameters and returns a tuple.

    This matches the parameters to a subcommand.
    The tuple is (base, subcommand_index, options, arguments).
    """
    parameters = parameters.strip()  # Safety strip

    if isinstance(command, commands.Shortcut):  # Fill replacement string
        logger.debug("Filling shortcut...")
        parameters = fill_shortcut(bot, command, parameters, message)
        command = command.command  # command is actually a Shortcut. Not confusing at all
        logger.debug("Shortcut filled to: %s", parameters)

    subcommand, options, arguments = match_subcommand(bot, command, parameters,
                                                      message)

    return (subcommand, options, arguments)
Ejemplo n.º 27
0
async def _start_scheduler(bot):
    """Starts the interal scheduler."""
    if bot.schedule_timer:  # Scheduler already running
        bot.schedule_timer.cancel()
        bot.schedule_timer = None
    cursor = data.db_select(bot,
                            from_arg='schedule',
                            additional='ORDER BY time ASC',
                            limit=1,
                            safe=False)
    result = cursor.fetchone()
    if result:
        delta = result[0] - time.time()
        logger.debug("_start_scheduler is starting a scheduled event.")
        bot.schedule_timer = asyncio.ensure_future(
            _schedule_timer(bot, result, delta))
    else:
        logger.debug(
            "_start_scheduler could not find a pending scheduled event.")
Ejemplo n.º 28
0
async def upload_to_discord(bot, fp, filename=None, rewind=True, close=False):
    """Uploads the given file-like object to the upload channel.

    If the upload channel is specified in the configuration files, files
    will be uploaded there. Otherwise, a new guild will be created, and
    used as the upload channel."""
    channel_id = configurations.get(bot, 'core', 'upload_channel')
    if not channel_id:  # Check to see if a guild was already created
        channel_id = data.get(bot, 'core', 'upload_channel')
    channel = data.get_channel(bot, channel_id, safe=True)

    # TODO: Remove. Guild creation via bots is a whitelisted process
    if channel is None:  # Create guild
        logger.debug("Creating guild for upload channel...")
        try:
            guild = await bot.create_guild('uploads')
        except Exception as e:
            raise CBException(
                "Failed to create upload guild. This bot is not whitelisted "
                "to create guilds.", e=e)
        data.add(bot, 'core', 'upload_channel', guild.id)
        channel = bot.get_channel(guild.id)

    if channel is None:  # Shouldn't happen
        raise CBException("Failed to get upload channel.")

    try:
        discord_file = discord.File(fp, filename=filename)
        message = await channel.send(file=discord_file)
        upload_url = message.attachments[0].url
    except Exception as e:
        raise CBException("Failed to upload file.", e=e)

    try:
        if close:
            fp.close()
        elif rewind:
            fp.seek(0)
    except:
        pass

    return upload_url
Ejemplo n.º 29
0
async def upload_to_discord(bot, fp, filename=None, rewind=True, close=False):
    """Uploads the given file-like object to the upload channel.

    If the upload channel is specified in the configuration files, files
    will be uploaded there. Otherwise, a new guild will be created, and
    used as the upload channel."""
    channel_id = configurations.get(bot, 'core', 'upload_channel')
    if not channel_id:  # Check to see if a guild was already created
        channel_id = data.get(bot, 'core', 'upload_channel')
    channel = data.get_channel(bot, channel_id, safe=True)

    if channel is None:  # Create guild
        logger.debug("Creating guild for upload channel...")
        try:
            guild = await bot.create_guild('uploads')
        except Exception as e:
            raise CBException(
                "Failed to create upload guild. This bot is not whitelisted "
                "to create guilds.",
                e=e)
        data.add(bot, 'core', 'upload_channel', guild.id)
        channel = bot.get_channel(guild.id)

    if channel is None:  # Shouldn't happen
        raise CBException("Failed to get upload channel.")

    try:
        discord_file = discord.File(fp, filename=filename)
        message = await channel.send(file=discord_file)
        upload_url = message.attachments[0].url
    except Exception as e:
        raise CBException("Failed to upload file.", e=e)

    try:
        if close:
            fp.close()
        elif rewind:
            fp.seek(0)
    except:
        pass

    return upload_url
Ejemplo n.º 30
0
def db_backup(bot, safe=True):
    """Use the Docker setup to backup the database."""
    try:
        logger.debug("Attemping to connect to the database container...")
        command = 'pg_dump -U postgres postgres > /external/data/db_dump.txt'
        host = 'db'
        port = 2345
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.1)
        s.connect((host, port))
        s.send(bytes(command, 'ascii'))
        s.close()
        time.sleep(1)
        logger.debug("Told database container to backup")
    except Exception as e:
        logger.warn("Failed to communicate with the database container: %s", e)
        if safe:
            return
        raise CBException("Failed to communicate with the database container.",
                          e=e)
Ejemplo n.º 31
0
async def member_banned(bot, guild, user):
    """Dumps logs when a user is banned."""
    try:
        _check_log_channel(bot, guild)
    except BotException:
        return

    # Ensure audit logs have been updated and pull ban information
    await asyncio.sleep(3)
    moderator_id = None
    details = '{0} (<@{0.id}>) was banned'.format(user)
    async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.ban):
        if entry.target == user:
            details += ' by {0} (<@{0.id}>): {1}'.format(
                entry.user, entry.reason or 'No reason provided')
            moderator_id = entry.user.id
            break
    logger.debug("Details: %s", details)

    # Show confirmation menu for logging
    await automated_dump_message(bot, guild, details, query=user.id, moderator_id=moderator_id)
Ejemplo n.º 32
0
async def _schedule_timer(bot, entry, delay):
    task_comparison = bot.schedule_timer
    await asyncio.sleep(0.5)
    logger.debug("Scheduler sleeping for %s seconds...", delay)
    await asyncio.sleep(delay)
    if task_comparison is not bot.schedule_timer:
        logger.debug("_schedule_timer was not cancelled! Cancelling this scheduler...")
        return
    if int(time.time() + 1) < entry.time:
        logger.warn("_schedule_timer was about to delete the entry early! Restarting loop...")
        asyncio.ensure_future(_start_scheduler(bot))
        return
    try:
        deleted = data.db_delete(
            bot, 'schedule', where_arg='id=%s', input_args=[entry.id], safe=False)
    except Exception as e:
        logger.warn("_schedule_timer failed to delete a schedule entry. %s", e)
    if deleted:
        try:
            logger.debug("_schedule_timer done sleeping for %s seconds!", delay)
            function = getattr(bot.plugins[entry.plugin], entry.function)
            late = delay < -60
            asyncio.ensure_future(function(
                bot, entry.time, entry.payload, entry.search,
                entry.destination, late, entry.info, entry.id))
        except Exception as e:
            logger.warn("Failed to execute scheduled function: %s", e)
    asyncio.ensure_future(_start_scheduler(bot))
Ejemplo n.º 33
0
def load_plugin(bot, plugin_name):
    directory = '{}/plugins'.format(bot.path)
    if plugin_name == 'base':
        raise CBException("Cannot (re)load base plugin.")

    if plugin_name in bot.plugins:
        logger.debug("Reloading plugin {}...".format(plugin_name))
        module = bot.plugins.pop(plugin_name)
        # importlib.reload(module)
        to_remove = []
        for base, command in bot.commands.items():
            if command.plugin is module:
                to_remove.append(base)
        for base in to_remove:
            del bot.commands[base]
        del module
    else:
        logger.debug("Loading plugin {}...".format(plugin_name))

    try:
        spec = importlib.util.spec_from_file_location(
            plugin_name, '{}/{}'.format(directory, plugin_name))
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
    except Exception as e:
        raise CBException("Failed to import external plugin.",
                          plugin_name,
                          e=e)
    else:
        bot.plugins[plugin_name] = module

    if plugin_name.lower().endswith('.py'):
        clean_name = plugin_name[:-3]
    else:
        clean_name = plugin_name

    add_configuration(bot, clean_name, plugin_name, module)
    add_manual(bot, clean_name, plugin_name)
    # TODO: Add manuals

    try:
        while command_spawner_functions:
            function = command_spawner_functions.pop()
            add_commands(bot, function(bot), module)
        while db_template_functions:
            function = db_template_functions.pop()
            template = function(bot)
            for name, value in template.items():
                data.db_add_template(bot, name, value)
        while command_load_functions:
            function = command_load_functions.pop()
            function(bot)
    except Exception as e:
        raise CBException("Failed to initialize external plugin.",
                          plugin_name,
                          e=e)
    logger.debug("Plugin {} loaded.".format(plugin_name))
Ejemplo n.º 34
0
def fill_shortcut(bot, shortcut, parameters, message):
    parameters = split_parameters(parameters, include_quotes=True)
    stripped_parameters = parameters[::2]
    arguments_dictionary = {}
    current_index = -1
    for current_index, current in enumerate(stripped_parameters):
        if current_index >= len(shortcut.args):
            raise CBException('Too many arguments.',
                              embed_fields=shortcut.help_embed_fields)
        else:
            arg = shortcut.args[current_index]
            if arg.argtype in (ArgTypes.SINGLE, ArgTypes.OPTIONAL):
                arguments_dictionary[arg.name] = current
            else:  # Instant finish grouped arguments
                if arg.argtype in (ArgTypes.SPLIT, ArgTypes.SPLIT_OPTIONAL):
                    arguments_dictionary[arg.name] = ''.join(
                        stripped_parameters[current_index:])
                else:  # Merged
                    arguments_dictionary[arg.name] = ''.join(
                        parameters[current_index * 2:])
                break
    # TODO: TEST THIS!
    logger.debug("Finished shortcut loop. %s", arguments_dictionary)
    if current_index < len(shortcut.args) - 1:  # Check for optional arguments
        arg = shortcut.args[current_index + 1]
        if arg.argtype is ArgTypes.OPTIONAL:
            while (arg and arg.argtype is ArgTypes.OPTIONAL
                   and current_index < len(shortcut.args)):
                arguments.append(arg.default)
                current_index += 1
                try:
                    arg = shortcut.args[current_index]
                except:
                    arg = None
        if arg and arg.argtype in (ArgTypes.SPLIT_OPTIONAL,
                                   ArgTypes.MERGED_OPTIONAL):
            arguments_dictionary[arg.name] = arg.default
        elif arg:
            raise CBException('Not enough arguments.',
                              embed_fields=shortcut.help_embed_fields)
    logger.debug("Finished checking for optional arguments. %s",
                 arguments_dictionary)
    for arg in shortcut.args:
        value = arguments_dictionary[arg.name]
        if value:
            logger.debug("Converting and checking 4")
            new_value = arg.convert_and_check(bot, message, value)
            arguments_dictionary[arg.name] = new_value
    return shortcut.replacement.format(**arguments_dictionary)
Ejemplo n.º 35
0
async def _schedule_timer(bot, raw_entry, delay):
    task_comparison = bot.schedule_timer
    await asyncio.sleep(0.5)
    logger.debug("_schedule_timer sleeping for %s seconds...", delay)
    await asyncio.sleep(delay)
    if task_comparison is not bot.schedule_timer:
        logger.debug(
            "_schedule_timer was not cancelled! Cancelling this scheduler...")
        return
    try:
        cursor = data.db_select(bot,
                                select_arg='min(time)',
                                from_arg='schedule')
        minimum_time = cursor.fetchone()[0]
        data.db_delete(bot,
                       'schedule',
                       where_arg='time=%s',
                       input_args=[minimum_time],
                       safe=False)
    except Exception as e:
        logger.warn("_schedule_timer failed to delete schedule entry. %s", e)
        raise e
    try:
        logger.debug("_schedule_timer done sleeping for %s seconds!", delay)
        scheduled_time, plugin, function, payload, search, destination, info = raw_entry
        if payload:
            payload = json.loads(payload)
        plugin = bot.plugins[plugin]
        function = getattr(plugin, function)
        late = delay < -60
        asyncio.ensure_future(
            function(bot, scheduled_time, payload, search, destination, late))
    except Exception as e:
        logger.warn("Failed to execute scheduled function: %s", e)
        raise e
    asyncio.ensure_future(_start_scheduler(bot))
Ejemplo n.º 36
0
def _build_dump_data(bot, logs, log_channel, details=None):
    """Builds dump data from the logs"""
    guild = log_channel.guild
    if not logs:
        return 0
    total_messages = 0
    members = {}
    channels = {}
    for channel_id, logged_messages in logs.items():

        messages = []
        for message_data in logged_messages:

            # Build message edit history
            edits = []
            for edit in message_data['history']:
                logger.debug("For ID: %s, content: %s", edit.id, edit.content)
                dt = (edit.edited_at or edit.created_at).replace(tzinfo=timezone.utc)
                edits.append({
                    'content': edit.content,
                    'embeds': [json.dumps(it.to_dict()) for it in edit.embeds],
                    'time': int(dt.timestamp())
                })
            edit = message_data['history'][-1]

            # Add user info if not found
            author_id = str(edit.author.id)
            if author_id not in members:
                dt = edit.author.joined_at.replace(tzinfo=timezone.utc)
                members[author_id] = {
                    'name': edit.author.name,
                    'discriminator': edit.author.discriminator,
                    'bot': edit.author.bot,
                    'id': str(edit.author.id),
                    'avatar': edit.author.avatar_url_as(static_format='png'),
                    'joined': int(dt.timestamp())
                }

            # Add message
            messages.append({
                'history': edits,
                'attachments': [it.url for it in edit.attachments],
                'author': author_id,
                'id': str(edit.id),
                'deleted': message_data['deleted']
            })

            total_messages += 1

        # Add all messages from the channel
        channel = guild.get_channel(channel_id)
        channel_name = channel.name if channel else 'Unknown channel ({})'.format(channel_id)
        channels[str(channel_id)] = {
            'messages': messages,
            'name': channel_name
        }

    # Build full dump
    return {
        'version': DATA_VERSION,
        'guild': {'name': guild.name, 'id': str(guild.id)},
        'members': members,
        'channels': channels,
        'generated': int(time.time()),
        'total': total_messages,
        'details': details
    }
Ejemplo n.º 37
0
    async def _menu(bot, context, response, result, timed_out):
        if not result and not timed_out:
            return

        # Timed out or skipped waiting period
        if timed_out or (result[0].emoji == '⏭'):
            response.embed.remove_field(2)
            response.embed.set_field_at(1, name='\u200b', value='Logging started.')
            await response.message.edit(embed=response.embed)
            asyncio.ensure_future(_dump(
                bot, dump_data, log_channel, details=details,
                query=query, moderator_id=moderator_id))

        # Cancelled
        elif result[0].emoji == '❌':
            response.embed.remove_field(2)
            response.embed.add_field(
                name='\u200b',
                value='Logging cancelled.')
            await response.message.edit(embed=response.embed)

        # Specify channels
        else:
            response.embed.set_field_at(
                2, name='\u200b', inline=False,
                value='**Type the channels you want to log, separated by spaces.**')
            await response.message.edit(embed=response.embed)
            try:
                await response.message.clear_reactions()
            except:
                pass

            # Read response
            kwargs = {
                'timeout': 300,
                'check': lambda m: m.author == result[1] and m.channel == context.channel
            }
            channels = []
            response.embed.remove_field(2)
            try:
                result = await bot.wait_for('message', **kwargs)
                for it in result.content.split():
                    channel = data.get_channel(bot, it, constraint=discord.TextChannel)
                    if channel.id not in channel_ids:
                        raise CBException("Channel {} not logged.".format(channel.mention))
                    channels.append(channel)
                try:
                    await result.delete()
                except:
                    pass
            except BotException as e:
                logger.debug("Error!")
                response.embed.set_field_at(
                    1, name='\u200b',
                    value='{}\nDefault channels will be logged.'.format(e.error_details))
            except Exception as e:
                logger.debug("Timeout!")
                response.embed.set_field_at(
                    1, name='\u200b',
                    value='Channel selection timed out. Default channels will be logged.')

            # Upload dump data
            await response.message.edit(embed=response.embed)
            await _dump(
                bot, dump_data, log_channel, details=details, query=query,
                moderator_id=moderator_id, logged_channels=channels)

        return False
Ejemplo n.º 38
0
def load_plugin(bot, plugin_name):
    directory = '{}/plugins'.format(bot.path)
    if plugin_name == 'base':
        raise CBException("Cannot (re)load the base plugin.")

    if plugin_name in bot.plugins:  # Prepare plugin for reloading
        logger.debug("Reloading plugin %s...", plugin_name)
        module = bot.plugins.pop(plugin_name)

        # Stop tasks
        tasks = asyncio.Task.all_tasks()
        pattern = re.compile('([^/(:\d>$)])+(?!.*\/)')
        for task in tasks:
            callback_info = task._repr_info()[1]
            plugin_name_test = pattern.search(callback_info).group(0)
            if plugin_name_test == plugin_name:
                logger.debug("Canceling task: %s",task)
                task.cancel()

        # Remove plugin functions that are registered to events
        for function_list in bot.event_functions.values():
            to_remove = []
            for function in function_list:
                if function.__module__ == plugin_name:
                    to_remove.append(function)
            for function in to_remove:
                function_list.remove(function)

        # Delete plugin commands
        to_remove = []
        for base, command in bot.commands.items():
            if command.plugin is module:
                to_remove.append(base)
        for base in to_remove:
            del bot.commands[base]

        del module

    else:
        logger.debug("Loading %s...", plugin_name)

    try:
        spec = importlib.util.spec_from_file_location(
            plugin_name, '{}/{}'.format(directory, plugin_name))
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
    except Exception as e:
        raise CBException("Failed to import external plugin.", plugin_name, e=e)
    else:
        bot.plugins[plugin_name] = module

    if plugin_name.lower().endswith('.py'):
        clean_name = plugin_name[:-3]
    else:
        clean_name = plugin_name

    add_configuration(bot, clean_name, plugin_name, module)
    add_manual(bot, clean_name, plugin_name)

    try:
        while command_spawner_functions:
            function = command_spawner_functions.pop()
            add_commands(bot, function(bot), module)
        while db_template_functions:
            function = db_template_functions.pop()
            template = function(bot)
            for name, value in template.items():
                data.db_add_template(bot, name, value)
        while command_load_functions:
            function = command_load_functions.pop()
            function(bot)
        while event_functions:
            event_name, function = event_functions.pop()
            if event_name not in bot.event_functions:
                bot.event_functions[event_name] = [function]
            else:
                bot.event_functions[event_name].append(function)
        while plugin_permissions:
            function = plugin_permissions.pop()
            utilities.add_bot_permissions(bot, plugin_name, **function(bot))
    except Exception as e:
        raise CBException("Failed to initialize external plugin.", plugin_name, e=e)
Ejemplo n.º 39
0
async def _violation_notification(bot, message, awoo_tier, send_message=True):
    """
    Logs the violation and (optionally) sends the user a notification.

    Standard notification: once per violation, up to 1 time
    None: 2 violations
    Silence notification: 1 violation

    Reset period for notifications is 1 minute.

    Stress indicates a number of users making a violation within a 60 second period.
    Tier 1: 3 members
    Tier 2: 5 members
    Tier 3: 8 members
    """

    author, channel = message.author, message.channel
    current_time = time.time()
    violation_data = data.get(bot,
                              __name__,
                              'user_violation',
                              user_id=author.id,
                              volatile=True)
    channel_violation_data = data.get(bot,
                                      __name__,
                                      'channel_violation',
                                      channel_id=channel.id,
                                      volatile=True)
    if not violation_data or current_time - violation_data['time'] >= 60:
        violation_data = {'time': 0, 'violations': 0}
        data.add(bot,
                 __name__,
                 'user_violation',
                 violation_data,
                 user_id=author.id,
                 volatile=True)
    if not channel_violation_data or current_time - channel_violation_data[
            'time'] >= 60:
        channel_violation_data = {
            'time': 0,
            'violators': set(),
            'sent_tier': 0
        }
        data.add(bot,
                 __name__,
                 'channel_violation',
                 channel_violation_data,
                 channel_id=channel.id,
                 volatile=True)
    violation_data['violations'] += 1
    violation_data['time'] = current_time
    channel_violation_data['violators'].add(author.id)
    channel_violation_data['time'] = current_time

    # Update table
    set_arg = 'debt = debt+%s, violations = violations+1'
    if awoo_tier == 2:
        set_arg += ', sneaky = sneaky+1'
    cursor = data.db_select(bot,
                            from_arg='awoo',
                            where_arg='user_id=%s',
                            input_args=[author.id])
    entry = cursor.fetchone() if cursor else None
    if entry:
        data.db_update(bot,
                       'awoo',
                       set_arg=set_arg,
                       where_arg='user_id=%s',
                       input_args=[fine, author.id])
    else:
        data.db_insert(
            bot,
            'awoo',
            input_args=[author.id, fine, 1, 1 if awoo_tier == 2 else 0])

    # Add a snarky message depending on the tier
    if awoo_tier == 2:  # Attempted bypass
        snark = random.choice(statements['bypass']) + '\n'
    elif awoo_tier == 3:  # Legalization plea
        snark = random.choice(statements['legalize']) + '\n'
    else:
        snark = ''

    # Notify user
    logger.debug("Violations: %s", violation_data['violations'])
    text = ''
    if violation_data['violations'] <= 1:
        text = "{}{} has been fined ${} for an awoo violation.".format(
            snark, author.mention, fine)
    elif violation_data['violations'] == 4:
        text = "{} {}".format(author.mention,
                              random.choice(statements['silence']))
    elif awoo_tier == 3 and violation_data[
            'violations'] <= 3:  # Legalization plea, but silent
        text = snark
    if send_message and text:
        await channel.send(content=text)
    else:
        await message.add_reaction(
            random.choice(['🚩', '🛑', '�', '⛔', '🚫']))

    # Stress
    violators, sent_tier = channel_violation_data[
        'violators'], channel_violation_data['sent_tier']
    if (len(violators) == 3 and sent_tier == 0
            or len(violators) == 5 and sent_tier == 1
            or len(violators) == 8 and sent_tier == 2):
        if send_message:
            await message.channel.send(
                random.choice(statements['stress'][sent_tier]))
        channel_violation_data['sent_tier'] += 1
Ejemplo n.º 40
0
def _build_dump_data(bot, logs, log_channel, details=None):
    """Builds dump data from the logs"""
    guild = log_channel.guild
    if not logs:
        return 0
    total_messages = 0
    members = {}
    channels = {}
    for channel_id, logged_messages in logs.items():

        messages = []
        for message_data in logged_messages:

            # Build message edit history
            edits = []
            for edit in message_data['history']:
                logger.debug("For ID: %s, content: %s", edit.id, edit.content)
                dt = (edit.edited_at
                      or edit.created_at).replace(tzinfo=timezone.utc)
                edits.append({
                    'content':
                    edit.content,
                    'embeds': [json.dumps(it.to_dict()) for it in edit.embeds],
                    'time':
                    int(dt.timestamp())
                })
            edit = message_data['history'][-1]

            # Add user info if not found
            author_id = str(edit.author.id)
            if author_id not in members:
                dt = edit.author.joined_at.replace(tzinfo=timezone.utc)
                members[author_id] = {
                    'name': edit.author.name,
                    'discriminator': edit.author.discriminator,
                    'bot': edit.author.bot,
                    'id': str(edit.author.id),
                    'avatar': edit.author.avatar_url_as(static_format='png'),
                    'joined': int(dt.timestamp())
                }

            # Add message
            messages.append({
                'history': edits,
                'attachments': [it.url for it in edit.attachments],
                'author': author_id,
                'id': str(edit.id),
                'deleted': message_data['deleted']
            })

            total_messages += 1

        # Add all messages from the channel
        channel = guild.get_channel(channel_id)
        channel_name = channel.name if channel else 'Unknown channel ({})'.format(
            channel_id)
        channels[str(channel_id)] = {
            'messages': messages,
            'name': channel_name
        }

    # Build full dump
    return {
        'version': DATA_VERSION,
        'guild': {
            'name': guild.name,
            'id': str(guild.id)
        },
        'members': members,
        'channels': channels,
        'generated': int(time.time()),
        'total': total_messages,
        'details': details
    }
Ejemplo n.º 41
0
        def __init__(self, path, debug, docker_mode):
            self.version = core_version
            self.date = core_date
            self.time = int(time.time())
            self.readable_time = time.strftime('%c')
            self.path = path
            self.debug = debug
            self.docker_mode = docker_mode

            logger.info("=== {0: ^40} ===".format("Starting up JshBot " + self.version))
            logger.info("=== {0: ^40} ===".format(self.readable_time))

            super().__init__()

            self.configurations = {}
            plugins.add_configuration(self, 'core', 'core', base)
            data.check_folders(self)

            logger.debug("Connecting to database...")
            self.db_templates = {}
            self.db_connection = None
            data.db_connect(self)

            logger.debug("Loading plugins...")
            self.data = {'global_users': {}, 'global_plugins': {}}
            self.volatile_data = {'global_users': {}, 'global_plugins': {}}
            self.data_changed = []
            self.tables_changed = []
            self.dump_exclusions = []
            self.plugins = OrderedDict()
            self.manuals = OrderedDict()
            self.commands = {}
            self.event_functions = {}
            plugins.add_plugins(self)

            config = self.configurations['core']
            self.edit_dictionary = {}
            self.spam_dictionary = {}
            self.spam_limit = config['command_limit']
            self.spam_timeout = config['command_limit_timeout']
            self.use_verbose_output = config['use_verbose_output']
            self.exception_messages = config['exception_messages']
            self.command_invokers = config['command_invokers']
            self.locked_commands = config['locked_commands']
            self.single_command = config['single_command']
            self.edit_timeout = config['edit_timeout']
            self.selfbot = config['selfbot_mode']
            self.owners = config['owners']
            self.schedule_timer = None
            self.response_deque = deque(maxlen=50)
            self.error_deque = deque(maxlen=50)
            self.maintenance_message = ''
            self.maintenance_mode = 0
            self.last_exception = None
            self.last_traceback = None
            self.last_response = None
            self.last_context = None
            self.fresh_boot = None
            self.ready = False
            self.extra = None

            # Extras
            config['token'] = '(redacted)'
            config['database_credentials'] = '(redacted)'
Ejemplo n.º 42
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)
Ejemplo n.º 43
0
    async def _menu(bot, context, response, result, timed_out):
        if not result and not timed_out:
            return

        # Timed out or skipped waiting period
        if timed_out or (result[0].emoji == '⏭'):
            response.embed.remove_field(2)
            response.embed.set_field_at(1,
                                        name='\u200b',
                                        value='Logging started.')
            await response.message.edit(embed=response.embed)
            asyncio.ensure_future(
                _dump(bot,
                      dump_data,
                      log_channel,
                      details=details,
                      query=query,
                      moderator_id=moderator_id))

        # Cancelled
        elif result[0].emoji == '❌':
            response.embed.remove_field(2)
            response.embed.add_field(name='\u200b', value='Logging cancelled.')
            await response.message.edit(embed=response.embed)

        # Specify channels
        else:
            response.embed.set_field_at(
                2,
                name='\u200b',
                inline=False,
                value=
                '**Type the channels you want to log, separated by spaces.**')
            await response.message.edit(embed=response.embed)
            try:
                await response.message.clear_reactions()
            except:
                pass

            # Read response
            kwargs = {
                'timeout':
                300,
                'check':
                lambda m: m.author == result[1] and m.channel == context.
                channel
            }
            channels = []
            response.embed.remove_field(2)
            try:
                result = await bot.wait_for('message', **kwargs)
                for it in result.content.split():
                    channel = data.get_channel(bot,
                                               it,
                                               constraint=discord.TextChannel)
                    if channel.id not in channel_ids:
                        raise CBException("Channel {} not logged.".format(
                            channel.mention))
                    channels.append(channel)
                try:
                    await result.delete()
                except:
                    pass
            except BotException as e:
                logger.debug("Error!")
                response.embed.set_field_at(
                    1,
                    name='\u200b',
                    value='{}\nDefault channels will be logged.'.format(
                        e.error_details))
            except Exception as e:
                logger.debug("Timeout!")
                response.embed.set_field_at(
                    1,
                    name='\u200b',
                    value=
                    'Channel selection timed out. Default channels will be logged.'
                )

            # Upload dump data
            await response.message.edit(embed=response.embed)
            await _dump(bot,
                        dump_data,
                        log_channel,
                        details=details,
                        query=query,
                        moderator_id=moderator_id,
                        logged_channels=channels)

        return False
Ejemplo n.º 44
0
Archivo: core.py Proyecto: sasma/JshBot
        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))
            '''
Ejemplo n.º 45
0
Archivo: core.py Proyecto: sasma/JshBot
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()
Ejemplo n.º 46
0
async def _violation_notification(bot, message, awoo_tier, send_message=True):
    """
    Logs the violation and (optionally) sends the user a notification.

    Standard notification: once per violation, up to 1 time
    None: 2 violations
    Silence notification: 1 violation

    Reset period for notifications is 1 minute.

    Stress indicates a number of users making a violation within a 60 second period.
    Tier 1: 3 members
    Tier 2: 5 members
    Tier 3: 8 members
    """

    author, channel = message.author, message.channel
    current_time = time.time()
    violation_data = data.get(
        bot, __name__, 'user_violation', user_id=author.id, volatile=True)
    channel_violation_data = data.get(
        bot, __name__, 'channel_violation', channel_id=channel.id, volatile=True)
    if not violation_data or current_time - violation_data['time'] >= 60:
        violation_data = {'time': 0, 'violations': 0}
        data.add(bot, __name__, 'user_violation', violation_data, user_id=author.id, volatile=True)
    if not channel_violation_data or current_time - channel_violation_data['time'] >= 60:
        channel_violation_data = {'time': 0, 'violators': set(), 'sent_tier': 0}
        data.add(
            bot, __name__, 'channel_violation', channel_violation_data,
            channel_id=channel.id, volatile=True)
    violation_data['violations'] += 1
    violation_data['time'] = current_time
    channel_violation_data['violators'].add(author.id)
    channel_violation_data['time'] = current_time

    # Update table
    set_arg = 'debt = debt+%s, violations = violations+1'
    if awoo_tier == 2:
        set_arg += ', sneaky = sneaky+1'
    cursor = data.db_select(bot, from_arg='awoo', where_arg='user_id=%s', input_args=[author.id])
    entry = cursor.fetchone() if cursor else None
    if entry:
        data.db_update(
            bot, 'awoo', set_arg=set_arg, where_arg='user_id=%s', input_args=[fine, author.id])
    else:
        data.db_insert(bot, 'awoo', input_args=[author.id, fine, 1, 1 if awoo_tier == 2 else 0])

    # Add a snarky message depending on the tier
    if awoo_tier == 2:  # Attempted bypass
        snark = random.choice(statements['bypass']) + '\n'
    elif awoo_tier == 3:  # Legalization plea
        snark = random.choice(statements['legalize']) + '\n'
    else:
        snark = ''

    # Notify user
    logger.debug("Violations: %s", violation_data['violations'])
    text = ''
    if violation_data['violations'] <= 1:
        text = "{}{} has been fined ${} for an awoo violation.".format(snark, author.mention, fine)
    elif violation_data['violations'] == 4:
        text = "{} {}".format(author.mention, random.choice(statements['silence']))
    elif awoo_tier == 3 and violation_data['violations'] <= 3:  # Legalization plea, but silent
        text = snark
    if send_message and text:
        await channel.send(content=text)
    else:
        await message.add_reaction(random.choice(['🚩', '🛑', '�', '⛔', '🚫']))

    # Stress
    violators, sent_tier = channel_violation_data['violators'], channel_violation_data['sent_tier']
    if (len(violators) == 3 and sent_tier == 0 or
            len(violators) == 5 and sent_tier == 1 or
            len(violators) == 8 and sent_tier == 2):
        if send_message:
            await message.channel.send(random.choice(statements['stress'][sent_tier]))
        channel_violation_data['sent_tier'] += 1