diff --git a/Jenkinsfile b/Jenkinsfile index 64b5fc2..e8b68b9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,7 +16,7 @@ pipeline { } environment { WORKSPACE_PATH = "/opt/nomad/alloc/${NOMAD_ALLOC_ID}/${NOMAD_TASK_NAME}${WORKSPACE}" - DESCRIPTION = "Another amazing piece of software written by Cronocide." + DESCRIPTION = "A chatbot for a local BlueBlubbles server." } stages { stage('Prepare') { diff --git a/README.md b/README.md index cf70b18..4eaa95c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ -# python-template +# bluebubbles_bot +## A chatbot for a local BlueBlubbles server. -Template repository for Docker image creation and deployment. +## Usage +``` -# Deployment Checklist +``` + +## Installation + +1. +2. +3. + +## Configuration + +``` + +``` + +## Justification -* Add Jenkins user to project as a Developer in Git -* Write the description in the Jenkinsfile env variable -* Add a private Github push mirror -* Rename the project in the README -* Delete this checklist from the README diff --git a/bin/bluebubbles_bot b/bin/bluebubbles_bot new file mode 100644 index 0000000..b9d84f6 --- /dev/null +++ b/bin/bluebubbles_bot @@ -0,0 +1,124 @@ +#!python3 + +import os +import sys +import yaml +import logging +import argparse +import bluebubbles_bot +import inspect +import importlib +# Import your custom libraries here + +_plugin_map = {} +_delegate_map = {} +_initialized_plugins = [] + +# Plugin loading code +plugin_class = 'bluebubbles_bot'.strip('-').capitalize() + 'Plugin' +def add_plugin(plugin,reload) : + """Adds a given plugin and instance, reinitializing one if it already exists and such is specified.""" + plugin_name = plugin.__module__.split('.')[-1] + if not reload and plugin_name in _plugin_map.keys(): + pass + else : + # We can't startup the plugin here because it hasn't been configured. We'll handle that at runtime. + try: + # Remove any intialized objects of the same name, forcing a reinitialization + _initialized_plugins.remove(_plugin_map[plugin_name]) + except: + pass + _plugin_map.update({plugin_name:plugin}) + +def use_plugins(plugins,reload=False) : + """Defines plugins that should be used in a lookup, optionally forcing them to reload.""" + # Verify data + if type(plugins) != list : + raise ValueError('argument \'plugins\' should be of type list') + for plugin in plugins : + # Check if the plugin is a string or a descendent of a bluebubbles_bot-plugin class + if type(plugin) != str and plugin_class not in [x.__name__ for x in inspect.getmro(plugin)] : + raise ValueError('unkown type for plugin') + # Find plugins by name using a default path + if type(plugin) == str : + available_plugins = [y for x,y in search_plugins().items() if x == plugin and plugin_class in [z.__name__ for z in inspect.getmro(y)]] + if len(available_plugins) == 0 : + raise FileNotFoundError(plugin + '.py not found') + plugin = available_plugins[0] + if plugin_class in [x.__name__ for x in inspect.getmro(plugin)] : + add_plugin(plugin,reload) + continue + +def get_plugins() : + """Returns a map of plugins configured and loaded.""" + return _plugin_map + +def search_plugins(directory=None) : + """Searches a given directory for compatible plugins and returns a map of available plugin names and classes.""" + if not directory : + directory = '/'.join(os.path.realpath(__file__).split('/')[:-1]) + '/' + 'plugins' + directory = os.path.normpath(os.path.expanduser(os.path.expandvars(directory))) + name_map = {} + candidates = {x.split('.')[0]:x for x in os.listdir(directory) if x.endswith('.py')} + for name,filename in candidates.items() : + try : + spec = importlib.util.spec_from_file_location(name, directory + '/' + filename) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + instance = getattr(mod,plugin_class) + name_map.update({filename.split('.')[0]:instance}) + except Exception as e : + # Handle plugin loading issues if desired + print("Unable to load plugin from " + filename + ": " + str(e)) + return name_map + +class LoggingFormatter(logging.Formatter): + def format(self, record): + module_max_width = 30 + datefmt='%Y/%m/%d/ %H:%M:%S' + level = f'[{record.levelname}]'.ljust(9) + if 'log_module' not in dir(record) : + modname = str(record.module)+'.'+str(record.name) + else : + modname = record.log_module + modname = (f'{modname}'[:module_max_width-1] + ']').ljust(module_max_width) + final = "%-7s %s [%s %s" % (self.formatTime(record, self.datefmt), level, modname, record.getMessage()) + return final + +if __name__ == '__main__': + + # Command-line client + # Define constants + config_template = {'bluebubbles_bot': {}} + + # Gather Argument options + EXAMPLE_TEXT='Example:\n\tbluebubbles_bot -h' + parser = argparse.ArgumentParser(epilog=EXAMPLE_TEXT,formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('-H', '--hosts', action='append', default=None, help='Collects arguments in an array.') + parser.add_argument('-d', '--dry-run', action='store_true', help='Store the existence of a variable.') + parser.add_argument('-l', '--log', action='store', help='Specify a file to log to.') + parser.add_argument('-v', '--verbose', action='count', help='Include verbose information in the output. Add \'v\'s for more output.',default=0) + args = parser.parse_args() + + log = logging.getLogger(__name__) + log = logging.LoggerAdapter(log,{'log_module':'bluebubbles_bot'}) + + # Configure logging + log_options = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] + if not args.verbose : + args.verbose = 0 + if args.verbose > 3 : + args.verbose = 3 + if args.log : + logging.basicConfig(level=log_options[args.verbose],filename=args.log) + logging.getLogger().addHandler(logging.StreamHandler(sys.stderr)) + else : + logging.basicConfig(level=log_options[args.verbose]) + logging.getLogger().handlers[0].setFormatter(LoggingFormatter()) + logging.propagate=True + + # Load plugins + available_plugins = search_plugins(directory='/'.join(os.path.realpath(__file__).split('/')[:-2]) + '/bluebubbles_bot/' + 'plugins') + use_plugins([x for x in available_plugins.values()]) + # Main functions + print('Hello World!') diff --git a/bluebubbles_bot/__init__.py b/bluebubbles_bot/__init__.py new file mode 100644 index 0000000..7295da9 --- /dev/null +++ b/bluebubbles_bot/__init__.py @@ -0,0 +1 @@ +from bluebubbles_bot.bluebubbles_bot import * diff --git a/bluebubbles_bot/bluebubbles_bot.py b/bluebubbles_bot/bluebubbles_bot.py new file mode 100644 index 0000000..04653ff --- /dev/null +++ b/bluebubbles_bot/bluebubbles_bot.py @@ -0,0 +1 @@ +# Write your modular code (classes, functions, etc) here. They'll be automatically imported in bin/bluebubbles_bot diff --git a/bluebubbles_bot/plugins/plugin.py b/bluebubbles_bot/plugins/plugin.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cd8f314 --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +from setuptools import setup, find_packages +from setuptools.command.install_scripts import install_scripts +from setuptools.command.install import install +from setuptools.command.develop import develop +from setuptools.command.egg_info import egg_info +from setuptools.command.build_ext import build_ext +import subprocess +import os +import glob + + +# From https://stackoverflow.com/questions/5932804/set-file-permissions-in-setup-py-file +# https://blog.niteo.co/setuptools-run-custom-code-in-setup-py/ +def customize(command) : + command_name = str(command.mro()[1].__name__).strip() + original_run = command.run + def run(self) : + # Run the rest of the installer first + original_run(self) + # Create a new subprocess to run the included shell script + print("Running " + command_name + " commands...") + current_dir_path = os.path.dirname(os.path.realpath(__file__)) + create_service_script_path = os.path.join(current_dir_path, 'setup.sh') + # stdout and stderr are combined in shell output + output=subprocess.run([create_service_script_path,command_name],stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout + print(output.decode('UTF-8')) + command.run = run + return command + +@customize +class CustomInstallCommand(install) : + pass + +@customize +class CustomDevelopCommand(develop) : + pass + +@customize +class CustomEggInfoCommand(egg_info) : + pass + +@customize +class CustomBuildExtCommand(build_ext) : + pass + +files = glob.glob('bluebubbles_bot/plugins/*.py') + +setup(name='bluebubbles_bot', + version='1.0.0', + url='', + license='Apache2', + author='Daniel Dayley', + author_email='github@cronocide.com', + description='A chatbot for a local BlueBlubbles server.', + packages=find_packages(exclude=['tests']), + package_data={"": ['plugins/*.py']}, + install_requires=['pyyaml',], + scripts=['bin/bluebubbles_bot'], + long_description=open('README.md').read(), + + zip_safe=True +)