From 2b10e99265f816f34e8530bac6b88a4ebf05b7f7 Mon Sep 17 00:00:00 2001 From: Roman Gräf Date: Sat, 5 Sep 2020 23:19:39 +0200 Subject: Initial commit --- .gitignore | 2 ++ README.md | 7 ++++ bot/__init__.py | 0 bot/__main__.py | 19 +++++++++++ bot/_bs/config.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ bot/_bs/default_tasks.py | 16 +++++++++ bot/_bs/load.py | 9 +++++ bot/_bs/systemd.py | 34 +++++++++++++++++++ bot/_bs/tasks.py | 10 ++++++ bot/bot.py | 17 ++++++++++ bot/config.py | 7 ++++ bot/tasks.py | 23 +++++++++++++ requirements.txt | 1 + 13 files changed, 230 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bot/__init__.py create mode 100644 bot/__main__.py create mode 100644 bot/_bs/config.py create mode 100644 bot/_bs/default_tasks.py create mode 100644 bot/_bs/load.py create mode 100644 bot/_bs/systemd.py create mode 100644 bot/_bs/tasks.py create mode 100644 bot/bot.py create mode 100644 bot/config.py create mode 100644 bot/tasks.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60a913f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +config.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..d59f218 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Discord Bootstrap +This is a template for my discord bots. Once finished, it should include the following features + + - [x] Automatic Configuration Questions + - [x] Automatic Dependency Installation + - [x] Automatic Systemd Integration + diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__main__.py b/bot/__main__.py new file mode 100644 index 0000000..915136e --- /dev/null +++ b/bot/__main__.py @@ -0,0 +1,19 @@ +from sys import argv + +from ._bs.load import load +from ._bs.tasks import tasks + + +def main(): + load('tasks') + if len(argv) <= 1: + print(f"Please provide a task to run: {' '.join(tasks.keys())}") + return 1 + to_run = tasks.get(argv[1]) + if not to_run: + print(f"Unknown task: {argv[1]}") + to_run() + + +if __name__ == '__main__': + exit(main()) diff --git a/bot/_bs/config.py b/bot/_bs/config.py new file mode 100644 index 0000000..8235e38 --- /dev/null +++ b/bot/_bs/config.py @@ -0,0 +1,85 @@ +from collections import namedtuple +from configparser import ConfigParser, DEFAULTSECT + +from .load import base_path + +parser = ConfigParser() +tracked_classes = [] +prop_attribs = namedtuple('prop_attribs', 'config_name required section') +all_props = [] +attr_map = {} +_UNSET = object() +has_read = False +config_file = str(base_path / 'config.ini') + + +def load_config(): + global has_read + if not has_read: + parser.read(config_file) + has_read = True + + +def prop_name(section, option): + return (section + '.' if section and section != DEFAULTSECT else '') + option + + +def section(section_name: str): + def x(c): + tracked_classes.append(c) + c._section = section_name + c._props = [] + for p in dir(c): + prop = getattr(c, p) + try: + prop = attr_map.get(prop, prop) + except: + continue + if hasattr(prop, 'config_name'): + s = prop.section or section_name + prop_info = prop_attribs(prop.config_name, prop.required, s) + c._props.append(prop_info) + all_props.append(prop_info) + return c() + + return x + + +def create_property(name, conv, section, fallback): + def get(self): + load_config() + s = section or getattr(self, '_section', None) or DEFAULTSECT + cv = parser.get(s, name, fallback=fallback) + if cv is _UNSET: + raise ValueError("Missing config option") + else: + return conv(cv) + + p = property(get) + attrs = prop_attribs(name, fallback is _UNSET, section) + attr_map[p] = attrs + return p + + +def required(name: str, conv=str, section=None): + return create_property(name, conv, section, _UNSET) + + +def default(name: str, default_value, conv=str, section=None): + return create_property(name, conv, section, default_value) + + +def prompt_missing_configs(): + load_config() + for p in all_props: + if parser.get(p.section, p.config_name, fallback=_UNSET) is _UNSET: + if not parser.has_section(p.section): + parser.add_section(p.section) + parser.set(p.section, p.config_name, + input(f"What value do you want for {prop_name(p.section, p.config_name)}: ")) + with open(config_file, 'w') as fp: + parser.write(fp) + print('Wrote config.ini') + + +__all__ = ['default', 'required', 'section', 'prompt_missing_configs'] diff --git a/bot/_bs/default_tasks.py b/bot/_bs/default_tasks.py new file mode 100644 index 0000000..b17aefa --- /dev/null +++ b/bot/_bs/default_tasks.py @@ -0,0 +1,16 @@ +import sys +from subprocess import call + +from .load import load +from .systemd import install_systemd + + +def install_requirements(): + call([sys.executable, '-m', 'pip', '-r', 'requirements.txt']) + + +def run_bot(): + load('bot').main() + + +__all__ = ['run_bot', 'install_systemd', 'install_requirements'] diff --git a/bot/_bs/load.py b/bot/_bs/load.py new file mode 100644 index 0000000..914a3cb --- /dev/null +++ b/bot/_bs/load.py @@ -0,0 +1,9 @@ +import importlib +import pathlib + + +def load(what: str): + return importlib.import_module('bot.' + what) + + +base_path = pathlib.Path(__file__).parent.parent.absolute() diff --git a/bot/_bs/systemd.py b/bot/_bs/systemd.py new file mode 100644 index 0000000..ce19cb6 --- /dev/null +++ b/bot/_bs/systemd.py @@ -0,0 +1,34 @@ +import sys + +from .load import base_path, load + +# language=ini +TEMPLATE = """ +[Unit] +Description = {description} + +[Service] +Type=simple +ExecStart={python_executable} -m bot run +WorkingDirectory={working_directory} +Restart=always + +[Install] +WantedBy=multi-user.target +""" + + +def generate_template(): + bot = load('bot') + return TEMPLATE.format( + python_executable=sys.executable, + working_directory=str(base_path), + description=bot.description, + ) + + +def install_systemd(): + bot = load('bot') + with open(f'/etc/systemd/system/{bot.name}.service', 'w') as fp: + fp.write(generate_template()) + print(f"Service created. Use systemd enable/start {bot.name} to start/enable the bot") diff --git a/bot/_bs/tasks.py b/bot/_bs/tasks.py new file mode 100644 index 0000000..29c3883 --- /dev/null +++ b/bot/_bs/tasks.py @@ -0,0 +1,10 @@ +from functools import partial + +tasks = {} + + +def task(name: str): + return partial(tasks.__setitem__, name) + + +__all__ = ['task'] diff --git a/bot/bot.py b/bot/bot.py new file mode 100644 index 0000000..4e1c884 --- /dev/null +++ b/bot/bot.py @@ -0,0 +1,17 @@ +from discord.ext import commands + +from bot.config import Config + +bot = commands.Bot(command_prefix=commands.when_mentioned_or('!')) + +name = "Bot Name" +description = "Bot Description" + + +@bot.command() +async def hello(ctx: commands.Context): + await ctx.send('Hello, World') + + +def main(): + bot.run(Config.token) diff --git a/bot/config.py b/bot/config.py new file mode 100644 index 0000000..1ec1bb2 --- /dev/null +++ b/bot/config.py @@ -0,0 +1,7 @@ +from ._bs.config import * + + +@section('discord') +class Config: + token = required('token') + admin = required('admin', int) diff --git a/bot/tasks.py b/bot/tasks.py new file mode 100644 index 0000000..67332d2 --- /dev/null +++ b/bot/tasks.py @@ -0,0 +1,23 @@ +from ._bs.config import prompt_missing_configs +from ._bs.default_tasks import install_requirements, run_bot, install_systemd +from ._bs.load import load +from ._bs.tasks import task + + +@task('run') +def run(): + run_bot() + + +@task('install') +def install(): + install_requirements() + prompt_missing_configs() + install_systemd() + + +@task('configurate') +@task('config') +def config(): + load('config') + prompt_missing_configs() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..844f49a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +discord.py -- cgit