aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md7
-rw-r--r--bot/__init__.py0
-rw-r--r--bot/__main__.py19
-rw-r--r--bot/_bs/config.py85
-rw-r--r--bot/_bs/default_tasks.py16
-rw-r--r--bot/_bs/load.py9
-rw-r--r--bot/_bs/systemd.py34
-rw-r--r--bot/_bs/tasks.py10
-rw-r--r--bot/bot.py17
-rw-r--r--bot/config.py7
-rw-r--r--bot/tasks.py23
-rw-r--r--requirements.txt1
13 files changed, 230 insertions, 0 deletions
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
--- /dev/null
+++ b/bot/__init__.py
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