import importlib
import logging
import sys
import warnings
from os import makedirs, path
from typing import Callable, Optional
from errbot.backend_plugin_manager import BackendPluginManager
from errbot.core import ErrBot
from errbot.logs import format_logs
from errbot.plugin_manager import BotPluginManager
from errbot.repo_manager import BotRepoManager
from errbot.storage.base import StoragePluginBase
from errbot.utils import PLUGINS_SUBDIR
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://errbot.io/repos.json"
[docs]
def bot_config_defaults(config: object) -> None:
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 = None # No user limit declared.
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: str,
logger: logging.Logger,
config: object,
restore: Optional[str] = None,
) -> ErrBot:
# 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 hasattr(config, "BOT_LOG_FILE") and 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:
sentry_integrations = []
try:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
except ImportError:
log.exception(
"You have BOT_LOG_SENTRY enabled, but I couldn't import modules "
"needed for Sentry integration. Did you install sentry-sdk? "
"(See https://docs.sentry.io/platforms/python for installation instructions)"
)
exit(-1)
sentry_logging = LoggingIntegration(
level=config.SENTRY_LOGLEVEL, event_level=config.SENTRY_EVENTLEVEL
)
sentry_integrations.append(sentry_logging)
if hasattr(config, "BOT_LOG_SENTRY_FLASK") and config.BOT_LOG_SENTRY_FLASK:
try:
from sentry_sdk.integrations.flask import FlaskIntegration
except ImportError:
log.exception(
"You have BOT_LOG_SENTRY enabled, but I couldn't import modules "
"needed for Sentry integration. Did you install sentry-sdk[flask]? "
"(See https://docs.sentry.io/platforms/python/flask for installation instructions)"
)
exit(-1)
sentry_integrations.append(FlaskIntegration())
sentry_options = getattr(config, "SENTRY_OPTIONS", {})
if hasattr(config, "SENTRY_TRANSPORT") and isinstance(
config.SENTRY_TRANSPORT, tuple
):
warnings.warn(
"SENTRY_TRANSPORT is deprecated. Please use SENTRY_OPTIONS instead.",
DeprecationWarning,
)
try:
mod = importlib.import_module(config.SENTRY_TRANSPORT[1])
transport = getattr(mod, config.SENTRY_TRANSPORT[0])
sentry_options["transport"] = transport
except ImportError:
log.exception(
f"Unable to import selected SENTRY_TRANSPORT - {config.SENTRY_TRANSPORT}"
)
exit(-1)
# merge options dict with dedicated SENTRY_DSN setting
sentry_kwargs = {
**sentry_options,
**{"dsn": config.SENTRY_DSN, "integrations": sentry_integrations},
}
sentry_sdk.init(**sentry_kwargs)
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,)
# Extra backend is expected to be a list type, convert string to list.
extra_backend = getattr(config, "BOT_EXTRA_BACKEND_DIR", [])
if isinstance(extra_backend, str):
extra_backend = [extra_backend]
backendpm = BackendPluginManager(
config, "errbot.backends", backend_name, ErrBot, CORE_BACKENDS, extra_backend
)
log.info(f"Found Backend plugin: {backendpm.plugin_info.name}")
repo_manager = BotRepoManager(storage_plugin, botplugins_dir, plugin_indexes)
try:
bot = backendpm.load_plugin()
botpm = BotPluginManager(
storage_plugin,
config.BOT_EXTRA_PLUGIN_DIR,
config.AUTOINSTALL_DEPS,
getattr(config, "CORE_PLUGINS", None),
lambda name, clazz: clazz(bot, name),
getattr(config, "PLUGINS_CALLBACK_ORDER", (None,)),
)
bot.attach_storage_plugin(storage_plugin)
bot.attach_repo_manager(repo_manager)
bot.attach_plugin_manager(botpm)
bot.initialize_backend_storage()
# 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(f"**** RESTORING the bot from {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_plugin_places(
repo_manager.get_all_repos_paths()
)
if errors:
startup_errors = "\n".join(errors.values())
log.error("Some plugins failed to load:\n%s", startup_errors)
bot._plugin_errors_during_startup = startup_errors
return bot
except Exception:
log.exception("Unable to load or configure the backend.")
exit(-1)
[docs]
def restore_bot_from_backup(backup_filename: str, *, bot, log: logging.Logger):
"""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: object) -> Callable:
"""
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 = BackendPluginManager(
config,
"errbot.storage",
storage_name,
StoragePluginBase,
CORE_STORAGE,
extra_storage_plugins_dir,
)
log.info(f"Found Storage plugin: {spm.plugin_info.name}.")
return spm.load_plugin()
[docs]
def bootstrap(
bot_class, logger: logging.Logger, config: object, restore: Optional[str] = None
) -> 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(f"Start serving commands from the {bot.mode} backend.")
bot.serve_forever()