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.")
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)
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.")
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)
def save_data(bot, force=False): """Saves all of the current data in the data dictionary. Does not save volatile_data, though. Backs up data if forced. """ if bot.data_changed or force: # Only save if something changed or forced # Loop through keys directory = bot.path + '/data/' if force: # Save all data for key, value in bot.data.items(): with open(directory + key + '.json', 'w') as current_file: try: json.dump(value, current_file, indent=4) except TypeError as e: logger.error('Failed to save data for %s: (TypeError) %s', key, e) # Check to see if any guild was removed files = os.listdir(directory) for check_file in files: if check_file.endswith('.json') and check_file[:-5] not in bot.data: logger.debug("Removing file {}".format(check_file)) os.remove(directory + check_file) else: # Save data that has changed for key in bot.data_changed: with open(directory + key + '.json', 'w') as current_file: json.dump(bot.data[key], current_file, indent=4) logger.debug("Saved {}".format(directory + key + '.json')) bot.data_changed = [] if force: utilities.make_backup(bot)
def 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)
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.")
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.")
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))
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.")
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()
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()
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)
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)
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()
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
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)'
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()
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
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
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.")
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))
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()
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
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)
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)
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.")
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
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
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)
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)
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))
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))
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)
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))
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 }
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
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)
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
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 }
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)'
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)
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
async def on_message(self, message, replacement_message=None): # Ensure bot can respond properly try: initial_data = self.can_respond(message) except Exception as e: # General error logger.error(e) logger.error(traceback.format_exc()) self.last_exception = e return if not initial_data: return # Ensure command is valid content = initial_data[0] elevation = 3 - (initial_data[4:0:-1] + [True]).index(True) split_content = content.split(' ', 1) if len(split_content) == 1: # No spaces split_content.append('') base, parameters = split_content base = base.lower() try: command = self.commands[base] except KeyError: logger.debug("Suitable command not found: " + base) return # Check that user is not spamming author_id = message.author.id direct = isinstance(message.channel, PrivateChannel) spam_value = self.spam_dictionary.get(author_id, 0) if elevation > 0 or direct: # Moderators ignore custom limit spam_limit = self.spam_limit else: spam_limit = min( self.spam_limit, data.get(self, 'core', 'spam_limit', guild_id=message.guild.id, default=self.spam_limit)) if spam_value >= spam_limit: if spam_value == spam_limit: self.spam_dictionary[author_id] = spam_limit + 1 plugins.broadcast_event(self, 'bot_on_user_ratelimit', message.author) await self.send_message( message.channel, "{0}, you appear to be issuing/editing " "commands too quickly. Please wait {1} seconds.". format(message.author.mention, self.spam_timeout)) return # Parse command and reply try: context = None with message.channel.typing(): logger.debug(message.author.name + ': ' + message.content) subcommand, options, arguments = parser.parse( self, command, parameters, message) context = self.Context( message, base, subcommand, options, arguments, subcommand.command.keywords, initial_data[0], elevation, message.guild, message.channel, message.author, direct, subcommand.index, subcommand.id, self) plugins.broadcast_event(self, 'bot_on_command', context) logger.info([subcommand, options, arguments]) response = await commands.execute(self, context) if response is None: response = Response() if self.selfbot and response.content: response.content = '\u200b' + response.content except Exception as e: # General error response = Response() destination = message.channel message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) else: # Attempt to respond send_arguments = response.get_send_kwargs(replacement_message) try: destination = response.destination if response.destination else message.channel message_reference = None if replacement_message: try: await replacement_message.edit(**send_arguments) message_reference = replacement_message except discord.NotFound: # Message deleted response = Response() message_reference = None elif (not response.is_empty() and not (self.selfbot and response.message_type is MessageTypes.REPLACE)): message_reference = await destination.send( **send_arguments) response.message = message_reference plugins.broadcast_event(self, 'bot_on_response', response, context) except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=replacement_message, command_editable=True) # Incremement the spam dictionary entry if author_id in self.spam_dictionary: self.spam_dictionary[author_id] += 1 else: self.spam_dictionary[author_id] = 1 # MessageTypes: # NORMAL - Normal. The issuing command can be edited. # PERMANENT - Message is not added to the edit dictionary. # REPLACE - Deletes the issuing command after 'extra' seconds. Defaults # to 0 seconds if 'extra' is not given. # ACTIVE - The message reference is passed back to the function defined # with 'extra_function'. If 'extra_function' is not defined, it will call # plugin.handle_active_message. # INTERACTIVE - Assembles reaction buttons given by extra['buttons'] and # calls 'extra_function' whenever one is pressed. # WAIT - Wait for event. Calls 'extra_function' with the result, or None # if the wait timed out. # # Only the NORMAL message type can be edited. response.message = message_reference if message_reference and isinstance(message_reference.channel, PrivateChannel): permissions = self.user.permissions_in( message_reference.channel) elif message_reference: permissions = message_reference.guild.me.permissions_in( message_reference.channel) else: permissions = None self.last_response = message_reference if response.message_type is MessageTypes.NORMAL: # Edited commands are handled in base.py if message_reference is None: # Forbidden exception return wait_time = self.edit_timeout if wait_time: self.edit_dictionary[str(message.id)] = message_reference await asyncio.sleep(wait_time) if str(message.id) in self.edit_dictionary: del self.edit_dictionary[str(message.id)] if message_reference.embeds: embed = message_reference.embeds[0] if embed.footer.text and embed.footer.text.startswith( '\u200b' * 3): embed.set_footer() try: await message_reference.edit(embed=embed) except: pass elif response.message_type is MessageTypes.REPLACE: try: if self.selfbot and not replacement_message: # Edit instead await message.edit(**send_arguments) else: if response.extra: await asyncio.sleep(response.extra) try: await message.delete(reason='Automatic') except: # Ignore permissions errors pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.ACTIVE: if message_reference is None: # Forbidden exception return try: await response.extra_function(self, context, response) except Exception as e: # General error message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.INTERACTIVE: try: buttons = response.extra['buttons'] kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 if 'check' not in kwargs: kwargs['check'] = (lambda r, u: r.message.id == message_reference.id and not u.bot) for button in buttons: await message_reference.add_reaction(button) reaction_check = await destination.get_message( message_reference.id) for reaction in reaction_check.reactions: if not reaction.me or reaction.count > 1: async for user in reaction.users(): if user != self.user and permissions.manage_messages: asyncio.ensure_future( message_reference.remove_reaction( reaction, user)) await response.extra_function(self, context, response, None, False) process_result = True while process_result is not False: try: if not permissions.manage_messages: add_task = self.wait_for( 'reaction_add', **kwargs) remove_task = self.wait_for( 'reaction_remove', **kwargs) done, pending = await asyncio.wait( [add_task, remove_task], return_when=FIRST_COMPLETED) result = next(iter(done)).result() for future in pending: future.cancel() else: # Can remove reactions result = await self.wait_for( 'reaction_add', **kwargs) if result[1] != self.user: asyncio.ensure_future( message_reference.remove_reaction( *result)) else: continue is_mod = data.is_mod(self, message.guild, result[1].id) if (response.extra.get('reactionlock', True) and not result[0].me or (response.extra.get('userlock', True) and not (result[1] == message.author or is_mod))): continue except (asyncio.futures.TimeoutError, asyncio.TimeoutError): await response.extra_function( self, context, response, None, True) process_result = False else: process_result = await response.extra_function( self, context, response, result, False) try: await response.message.clear_reactions() except: pass except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference elif response.message_type is MessageTypes.WAIT: try: kwargs = response.extra.get('kwargs', {}) if 'timeout' not in kwargs: kwargs['timeout'] = 300 process_result = True while process_result is not False: try: result = await self.wait_for( response.extra['event'], **kwargs) except asyncio.TimeoutError: await response.extra_function( self, context, response, None) process_result = False else: process_result = await response.extra_function( self, context, response, result) if not response.extra.get('loop', False): process_result = False except Exception as e: message_reference = await self.handle_error( e, message, context, response, edit=message_reference) self.last_response = message_reference else: logger.error("Unknown message type: {}".format( response.message_type)) '''
def start(start_file=None, debug=False): if start_file: path = os.path.split(os.path.realpath(start_file))[0] logger.debug("Setting directory to " + path) else: # Use Docker setup path = '/external' logger.info("Bot running in Docker mode.") logger.debug("Using Docker setup path, " + path) try: config_file_location = path + '/config/core-config.yaml' with open(config_file_location, 'rb') as config_file: config = yaml.load(config_file) selfbot_mode, token = config['selfbot_mode'], config['token'] except Exception as e: logger.error("Could not determine token /or selfbot mode.") raise e if selfbot_mode: client_type = discord.Client logger.debug("Using standard client (selfbot enabled).") else: client_type = discord.AutoShardedClient logger.debug("Using autosharded client (selfbot disabled).") if debug: log_file = '{}/temp/logs.txt'.format(path) if os.path.isfile(log_file): shutil.copy2(log_file, '{}/temp/last_logs.txt'.format(path)) logging.basicConfig(level=logging.DEBUG, handlers=[ RotatingFileHandler(log_file, maxBytes=1000000, backupCount=3), logging.StreamHandler() ]) def safe_exit(): loop = asyncio.get_event_loop() try: # From discord.py client.run loop.run_until_complete(bot.logout()) pending = asyncio.Task.all_tasks() gathered = asyncio.gather(*pending) except Exception as e: logger.error("Failed to log out. %s", e) try: gathered.cancel() loop.run_until_complete(gathered) gathered.exception() except: pass logger.warn("Bot disconnected. Shutting down...") bot.shutdown() # Calls sys.exit def exception_handler(loop, context): e = context.get('exception') if e and e.__traceback__: traceback_text = ''.join(traceback.format_tb(e.__traceback__)) else: traceback_text = traceback.format_exc() if not traceback_text: traceback_text = '(No traceback available)' error_message = '{}\n{}'.format(e, traceback_text) logger.error("An uncaught exception occurred.\n" + error_message) with open(path + '/temp/error.txt', 'w') as error_file: error_file.write(error_message) logger.error("Error file written.") if bot.is_closed(): safe_exit() loop = asyncio.get_event_loop() bot = get_new_bot(client_type, path, debug) start_task = bot.start(token, bot=not selfbot_mode) loop.set_exception_handler(exception_handler) try: loop.run_until_complete(start_task) except KeyboardInterrupt: logger.warn("Interrupted!") safe_exit()
async def _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