from os import path, makedirs
import importlib
import logging
import sys
import ast
from errbot.core import ErrBot
from errbot.plugin_manager import BotPluginManager
from errbot.repo_manager import BotRepoManager
from errbot.specific_plugin_manager import SpecificPluginManager
from errbot.storage.base import StoragePluginBase
from errbot.utils import PLUGINS_SUBDIR
from errbot.logs import format_logs
log = logging.getLogger(__name__)
HERE = path.dirname(path.abspath(__file__))
CORE_BACKENDS = path.join(HERE, 'backends')
CORE_STORAGE = path.join(HERE, 'storage')
PLUGIN_DEFAULT_INDEX = 'https://repos.errbot.io/repos.json'
[docs]def bot_config_defaults(config):
if not hasattr(config, 'ACCESS_CONTROLS_DEFAULT'):
config.ACCESS_CONTROLS_DEFAULT = {}
if not hasattr(config, 'ACCESS_CONTROLS'):
config.ACCESS_CONTROLS = {}
if not hasattr(config, 'HIDE_RESTRICTED_COMMANDS'):
config.HIDE_RESTRICTED_COMMANDS = False
if not hasattr(config, 'HIDE_RESTRICTED_ACCESS'):
config.HIDE_RESTRICTED_ACCESS = False
if not hasattr(config, 'BOT_PREFIX_OPTIONAL_ON_CHAT'):
config.BOT_PREFIX_OPTIONAL_ON_CHAT = False
if not hasattr(config, 'BOT_PREFIX'):
config.BOT_PREFIX = '!'
if not hasattr(config, 'BOT_ALT_PREFIXES'):
config.BOT_ALT_PREFIXES = ()
if not hasattr(config, 'BOT_ALT_PREFIX_SEPARATORS'):
config.BOT_ALT_PREFIX_SEPARATORS = ()
if not hasattr(config, 'BOT_ALT_PREFIX_CASEINSENSITIVE'):
config.BOT_ALT_PREFIX_CASEINSENSITIVE = False
if not hasattr(config, 'DIVERT_TO_PRIVATE'):
config.DIVERT_TO_PRIVATE = ()
if not hasattr(config, 'DIVERT_TO_THREAD'):
config.DIVERT_TO_THREAD = ()
if not hasattr(config, 'MESSAGE_SIZE_LIMIT'):
config.MESSAGE_SIZE_LIMIT = 10000 # Corresponds with what HipChat accepts
if not hasattr(config, 'GROUPCHAT_NICK_PREFIXED'):
config.GROUPCHAT_NICK_PREFIXED = False
if not hasattr(config, 'AUTOINSTALL_DEPS'):
config.AUTOINSTALL_DEPS = True
if not hasattr(config, 'SUPPRESS_CMD_NOT_FOUND'):
config.SUPPRESS_CMD_NOT_FOUND = False
if not hasattr(config, 'BOT_ASYNC'):
config.BOT_ASYNC = True
if not hasattr(config, 'BOT_ASYNC_POOLSIZE'):
config.BOT_ASYNC_POOLSIZE = 10
if not hasattr(config, 'CHATROOM_PRESENCE'):
config.CHATROOM_PRESENCE = ()
if not hasattr(config, 'CHATROOM_RELAY'):
config.CHATROOM_RELAY = ()
if not hasattr(config, 'REVERSE_CHATROOM_RELAY'):
config.REVERSE_CHATROOM_RELAY = ()
if not hasattr(config, 'CHATROOM_FN'):
config.CHATROOM_FN = 'Errbot'
if not hasattr(config, 'TEXT_DEMO_MODE'):
config.TEXT_DEMO_MODE = True
if not hasattr(config, 'BOT_ADMINS'):
raise ValueError('BOT_ADMINS missing from config.py.')
if not hasattr(config, 'TEXT_COLOR_THEME'):
config.TEXT_COLOR_THEME = 'light'
if not hasattr(config, 'BOT_ADMINS_NOTIFICATIONS'):
config.BOT_ADMINS_NOTIFICATIONS = config.BOT_ADMINS
[docs]def setup_bot(backend_name, logger, config, restore=None):
# from here the environment is supposed to be set (daemon / non daemon,
# config.py in the python path )
bot_config_defaults(config)
if hasattr(config, 'BOT_LOG_FORMATTER'):
format_logs(formatter=config.BOT_LOG_FORMATTER)
else:
format_logs(theme_color=config.TEXT_COLOR_THEME)
if config.BOT_LOG_FILE:
hdlr = logging.FileHandler(config.BOT_LOG_FILE)
hdlr.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s %(name)-25s %(message)s"))
logger.addHandler(hdlr)
if hasattr(config, 'BOT_LOG_SENTRY') and config.BOT_LOG_SENTRY:
try:
from raven.handlers.logging import SentryHandler
except ImportError:
log.exception(
"You have BOT_LOG_SENTRY enabled, but I couldn't import modules "
"needed for Sentry integration. Did you install raven? "
"(See http://raven.readthedocs.org/en/latest/install/index.html "
"for installation instructions)"
)
exit(-1)
if hasattr(config, 'SENTRY_TRANSPORT') and isinstance(config.SENTRY_TRANSPORT, tuple):
try:
mod = importlib.import_module(config.SENTRY_TRANSPORT[1])
transport = getattr(mod, config.SENTRY_TRANSPORT[0])
except ImportError:
log.exception(
"Unable to import selected SENTRY_TRANSPORT - {transport}".format(transport=config.SENTRY_TRANSPORT)
)
exit(-1)
sentryhandler = SentryHandler(config.SENTRY_DSN,
level=config.SENTRY_LOGLEVEL,
transport=transport)
else:
sentryhandler = SentryHandler(config.SENTRY_DSN, level=config.SENTRY_LOGLEVEL)
logger.addHandler(sentryhandler)
logger.setLevel(config.BOT_LOG_LEVEL)
storage_plugin = get_storage_plugin(config)
# init the botplugin manager
botplugins_dir = path.join(config.BOT_DATA_DIR, PLUGINS_SUBDIR)
if not path.exists(botplugins_dir):
makedirs(botplugins_dir, mode=0o755)
plugin_indexes = getattr(config, 'BOT_PLUGIN_INDEXES', (PLUGIN_DEFAULT_INDEX,))
if isinstance(plugin_indexes, str):
plugin_indexes = (plugin_indexes, )
repo_manager = BotRepoManager(storage_plugin,
botplugins_dir,
plugin_indexes)
botpm = BotPluginManager(storage_plugin,
repo_manager,
config.BOT_EXTRA_PLUGIN_DIR,
config.AUTOINSTALL_DEPS,
getattr(config, 'CORE_PLUGINS', None),
getattr(config, 'PLUGINS_CALLBACK_ORDER', (None, )))
# init the backend manager & the bot
backendpm = bpm_from_config(config)
backend_plug = backendpm.get_candidate(backend_name)
log.info("Found Backend plugin: '%s'\n\t\t\t\t\t\tDescription: %s" % (backend_plug.name, backend_plug.description))
try:
bot = backendpm.get_plugin_by_name(backend_name)
bot.attach_storage_plugin(storage_plugin)
bot.attach_repo_manager(repo_manager)
bot.attach_plugin_manager(botpm)
bot.initialize_backend_storage()
except Exception:
log.exception("Unable to load or configure the backend.")
exit(-1)
# restore the bot from the restore script
if restore:
# Prepare the context for the restore script
if 'repos' in bot:
log.fatal('You cannot restore onto a non empty bot.')
sys.exit(-1)
log.info('**** RESTORING the bot from %s' % restore)
restore_bot_from_backup(restore, bot=bot, log=log)
print('Restore complete. You can restart the bot normally')
sys.exit(0)
errors = bot.plugin_manager.update_dynamic_plugins()
if errors:
log.error('Some plugins failed to load:\n' + '\n'.join(errors.values()))
bot._plugin_errors_during_startup = "\n".join(errors.values())
return bot
[docs]def restore_bot_from_backup(backup_filename, *, bot, log):
"""Restores the given bot by executing the 'backup' script.
The backup file is a python script which manually execute a series of commands on the bot to restore it
to its previous state.
:param backup_filename: the full path to the backup script.
:param bot: the bot instance to restore
:param log: logger to use during the restoration process
"""
with open(backup_filename) as f:
exec(f.read(), {'log': log, 'bot': bot})
bot.close_storage()
[docs]def get_storage_plugin(config):
"""
Find and load the storage plugin
:param config: the bot configuration.
:return: the storage plugin
"""
storage_name = getattr(config, 'STORAGE', 'Shelf')
extra_storage_plugins_dir = getattr(config, 'BOT_EXTRA_STORAGE_PLUGINS_DIR', None)
spm = SpecificPluginManager(config, 'storage', StoragePluginBase, CORE_STORAGE, extra_storage_plugins_dir)
storage_pluginfo = spm.get_candidate(storage_name)
log.info("Found Storage plugin: '%s'\nDescription: %s" % (storage_pluginfo.name, storage_pluginfo.description))
storage_plugin = spm.get_plugin_by_name(storage_name)
return storage_plugin
[docs]def bpm_from_config(config):
"""Creates a backend plugin manager from a given config."""
extra = getattr(config, 'BOT_EXTRA_BACKEND_DIR', [])
return SpecificPluginManager(
config,
'backends',
ErrBot,
CORE_BACKENDS,
extra_search_dirs=extra
)
[docs]def enumerate_backends(config):
""" Returns all the backends found for the given config.
"""
bpm = bpm_from_config(config)
return [plug.name for (_, _, plug) in bpm.getPluginCandidates()]
[docs]def bootstrap(bot_class, logger, config, restore=None):
"""
Main starting point of Errbot.
:param bot_class: The backend class inheriting from Errbot you want to start.
:param logger: The logger you want to use.
:param config: The config.py module.
:param restore: Start Errbot in restore mode (from a backup).
"""
bot = setup_bot(bot_class, logger, config, restore)
log.debug('Start serving commands from the %s backend' % bot.mode)
bot.serve_forever()