Source code for glados.plugin

from typing import Callable, Dict, Union
import yaml
import glob
import logging
import requests

from pathlib import Path

import importlib

from glados import (
    GladosBot,
    RouteType,
    GladosRoute,
    BOT_ROUTES,
    GladosPathExistsError,
    GladosBotNotFoundError,
    GladosError,
    GladosRequest,
    VERIFY_ROUTES,
    EventRoutes,
    PyJSON,
)

from slack.web.classes.messages import Message
from slack.web.classes.objects import MarkdownTextObject, TextObject, PlainTextObject

SLACK_MESSAGE_TYPES = [Message, MarkdownTextObject, TextObject, PlainTextObject]


[docs]class PluginBotConfig: def __init__(self, name="NOT SET"): self.name = name
[docs] def to_dict(self): return dict(name=self.name)
# TODO # Read the plugin config # read the user config # running_config = plugin_config.update(user_config
[docs]class PluginConfig: def __init__( self, name, config_file, module=None, enabled=False, bot=None, **kwargs ): if not bot: bot = dict() self.name = name self.module = module self.enabled = enabled self.bot = PluginBotConfig(**bot) self.config_file = config_file self.config = PyJSON(kwargs) package = config_file.replace("/", ".") package = package.replace(".config.yaml", "") self.package = package
[docs] def update(self, config: "PluginConfig", use_base_module: bool = True): """Update a config object using the default values from the config object passed in. Parameters ---------- config : PluginConfig the config object to use as the base. By default the module property will be set from the base config object only use_base_module: bool if set true use the value of module and package from the base config object only. Returns ------- """ config = config.__dict__.copy() self_config = self.__dict__ if use_base_module: self_config.pop("module") self_config.pop("package") config.update(self_config) self.__dict__ = config
[docs] def to_dict(self, user_config_only=True): config = dict(enabled=self.enabled, bot=self.bot.to_dict()) if not user_config_only: config["module"] = self.module return {self.name: config}
[docs] def to_yaml(self, user_config_only=True): return yaml.dump(self.to_dict(user_config_only))
[docs]class PluginImporter: def __init__(self, plugins_folder: str, plugins_config_folder: str): self.plugins = dict() self.plugins_folder = plugins_folder self.plugins_config_folder = plugins_config_folder self.config_files = list() self.plugin_configs = dict() # type: Dict[str, PluginConfig]
[docs] def discover_plugins(self): """Discover all plugin config files in the plugins folder Returns ------- list: list of all yaml config files """ config_files = glob.glob(f"{self.plugins_folder}/**/*.yaml", recursive=True) self.config_files = config_files
[docs] def load_discovered_plugins_config(self, write_to_user_config=True): """Load all the yaml configs for the plugins""" plugin_package_config = None plugin_user_config = None logging.debug("starting import of plugins") for config_file in self.config_files: # Read the plugin package config plugin_name = None with open(config_file) as file: c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: logging.critical( f"zero or more than one object in config file: {config_file}" ) continue plugin_name = list(c.keys())[0] c[plugin_name]["config_file"] = config_file plugin_package_config = PluginConfig(plugin_name, **c[plugin_name]) if plugin_name is None: logging.critical( f"invalid or missing plugin name. config file: {config_file}" ) continue user_config_path = Path(self.plugins_config_folder, f"{plugin_name}.yaml") # Write defaults to user file if not user_config_path.is_file() and write_to_user_config: with open(user_config_path, "w") as file: plugin_package_config.enabled = False yaml.dump(plugin_package_config.to_dict(), file) elif not user_config_path.is_file() and not write_to_user_config: logging.warning(f"no user plugin config for {plugin_name}. skipping.") continue with open(user_config_path) as file: c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: logging.critical( f"zero or more than one object in config file: {config_file}" ) continue c[plugin_name]["config_file"] = str(user_config_path) plugin_user_config = PluginConfig(plugin_name, **c[plugin_name]) plugin_user_config.update(plugin_package_config) self.plugin_configs[plugin_name] = plugin_user_config
# TODO(zpriddy): Filter out warnings and errors if importing plugins in a limited way.
[docs] def import_discovered_plugins(self, bots: Dict[str, GladosBot]): """Import all discovered plugins and store them in self.plugins. Parameters ---------- bots : Dict[str, GladosBot] dict of all the imported bots Returns ------- None: the results are updated in self.plugins """ for plugin_name, plugin_config in self.plugin_configs.items(): if not plugin_config.enabled: logging.warning(f"plugin {plugin_name} is disabled") continue plugin_config.name = plugin_name logging.info(f"importing plugin: {plugin_name}") module = importlib.import_module(plugin_config.package) # Check if required bot is imported def get_required_bot( bot_name: str, bots: Dict[str, GladosBot] ) -> Union[None, GladosBot]: if not bot_name: raise GladosError(f"no bot name set for plugin: {plugin_name}") bot = bots.get(bot_name) if not bot: logging.error( f"bot: {bot_name} is not found. disabling plugin: {plugin_name}" ) raise GladosBotNotFoundError( f"bot: {bot_name} is not found as required for {plugin_name}" ) return bot try: bot = get_required_bot(plugin_config.bot.name, bots) except GladosError as e: logging.error(f"{e} :: disabling plugin: {plugin_name}") self.plugin_configs[plugin_name].enabled = False continue plugin = getattr(module, plugin_config.module)(plugin_config, bot) self.plugins[plugin_name] = plugin
[docs]class GladosPlugin: """Parent class for a GLaDOS Plugin Parameters ---------- name : str the name of the plugin bot : GladosBot the GLaDOS bot that this plugin will use """ def __init__(self, config: PluginConfig, bot: GladosBot, **kwargs): self.name = config.name self._config = config self.config = config.config self.bot = bot self._routes = dict() # type: Dict[int, Dict[str, GladosRoute]] for route in RouteType._member_names_: self._routes[ RouteType[route].value ] = dict() # type: Dict[str, GladosRoute]
[docs] def add_route( self, route_type: RouteType, route: Union[EventRoutes, str], function: Callable ): """Add a new route to the plugin Parameters ---------- route_type : RouteType what type of route this is this route : Union[EventRoutes, str] what is the route to be added function : Callable the function to be executed when this route runs Returns ------- """ if type(route) is EventRoutes: route = route.name new_route = GladosRoute(route_type, route, function) if route_type in BOT_ROUTES: new_route.route = f"{self.bot.name}_{route}" if new_route.route in self._routes[new_route.route_type.value]: raise GladosPathExistsError( f"a route with the name of {new_route.route} already exists in the route type: {new_route.route_type.name}" ) self._routes[new_route.route_type.value][new_route.route] = new_route
[docs] def send_request(self, request: GladosRequest, **kwargs): """This is the function to be called when sending a request to a plugin. This function is responsible for validating the slack signature if needed. It also returns and empty string if the function called returns None. Parameters ---------- request : GladosRequest the request object to be sent kwargs : Returns ------- """ if request.route_type in VERIFY_ROUTES: self.bot.validate_slack_signature(request) response = self._routes[request.route_type.value][request.route].function( request, **kwargs ) if response is None: # TODO(zpriddy): add logging. return "" if request.route_type is RouteType.Interaction and request.response_url: if type(response) is str: self.respond_to_url(request, response) if type(response) in SLACK_MESSAGE_TYPES: response = response.to_dict() if type(response) is dict: self.respond_to_url(request, **response) return response
[docs] def respond_to_url(self, request: GladosRequest, text: str, **kwargs): if not request.response_url: logging.error("no response_url provided in request.") return kwargs["text"] = text r = requests.post(request.response_url, json=kwargs) logging.info(f"slack response: {r}")
@property def routes(self): """List all routes for the plugin. Returns ------- """ routes = list() [ routes.extend(route_object) for route_object in [ list(route.values()) for route in [route_type for route_type in self._routes.values()] ] ] return routes