Arrange precedence of config loading

Closes https://git.autonomic.zone/decentral1se/xbotlib/issues/14.
This commit is contained in:
Luke Murphy 2021-01-14 22:26:13 +01:00
parent fbd72b55fe
commit 5b43a2cde2
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
3 changed files with 117 additions and 49 deletions

View File

@ -3,6 +3,9 @@
# xbotlib 0.8.0 (2021-01-14) # xbotlib 0.8.0 (2021-01-14)
- Support not providing response implementation ([#18](https://git.autonomic.zone/decentral1se/xbotlib/issues/18)) - Support not providing response implementation ([#18](https://git.autonomic.zone/decentral1se/xbotlib/issues/18))
- Arrange precedence logic for config loading ([#14](https://git.autonomic.zone/decentral1se/xbotlib/issues/14))
- Remove `--no-input` option and detect it automatically ([#14](https://git.autonomic.zone/decentral1se/xbotlib/issues/14))
- Refer to `jid` as `account` from now on both internally and externally ([#14](https://git.autonomic.zone/decentral1se/xbotlib/issues/14))
# xbotlib 0.7.1 (2021-01-13) # xbotlib 0.7.1 (2021-01-13)

View File

@ -22,8 +22,10 @@ $ pip install xbotlib
## Example ## Example
Put the following in a `echo.py` file. `xbotlib` provides a number of example Put the following in a `echo.py` file.
bots which you can use to get moving fast and try things out.
`xbotlib` provides a number of example bots which you can use to get moving
fast and try things out.
```python ```python
from xbotlib import EchoBot from xbotlib import EchoBot
@ -31,10 +33,10 @@ from xbotlib import EchoBot
EchotBot() EchotBot()
``` ```
And then `python echo.py`. You will be asked a few questions like which account And then `python echo.py`. You will be asked a few questions in order to load
details your bot will be using. the account details that your bot will be using. This will generate a
`bot.conf` file in the same working directory for further use. See the
This will generate a `bot.conf` file in the same working directory for further use. [configuration](#configure-your-bot) section for more.
Here's the code for the `EchoBot`. Here's the code for the `EchoBot`.
@ -95,15 +97,29 @@ Attributes:
## Configure your bot ## Configure your bot
All the ways you can pass configuration details to your bot.
### Using the bot.conf
If you run simply run your Python script which contains the bot then `xbotlib`
will generate a configuration for you by asking a few questions. This is the
simplest way to run your bot locally.
### Using the command-line interface
Every bot accepts a number of comand-line arguments to load configuration. You
can use the `--help` option to see what is available (e.g. `python bot.py --help`).
### Using the environment ### Using the environment
You can pass the `--no-input` option to your script invocation (e.g. `python bot.py --no-input`). `xbotlib` will try to read the following configuration values from the
environment if it cannot read them from a configuration file or the
command-line interface. This can be useful when doing remote server
deployments.
`xbotlib` will try to read the following configuration values from the environment. - **XBOT_ACCOUNT**: The bot account
- **XBOT_PASSWORD**: The bot password
- **XBOT_JID**: The username of the bot account - **XBOT_NICK**: The bot nickname
- **XBOT_PASSWORD**: The password of the bot account
- **XBOT_NICK**: The nickname that the bot uses
## Roadmap ## Roadmap

View File

@ -1,12 +1,13 @@
"""XMPP bots for humans.""" """XMPP bots for humans."""
from argparse import ArgumentParser, BooleanOptionalAction from argparse import ArgumentParser
from configparser import ConfigParser from configparser import ConfigParser
from getpass import getpass from getpass import getpass
from logging import DEBUG, INFO, basicConfig, getLogger from logging import DEBUG, INFO, basicConfig, getLogger
from os import environ from os import environ
from os.path import exists from os.path import exists
from pathlib import Path from pathlib import Path
from sys import exit, stdout
from slixmpp import ClientXMPP from slixmpp import ClientXMPP
@ -42,7 +43,29 @@ class SimpleMessage:
return self.message["type"] return self.message["type"]
class Config:
"""Bot file configuration."""
def __init__(self, config):
self.config = config
self.section = config["bot"] if "bot" in config else {}
@property
def account(self):
return self.section.get("account", None)
@property
def password(self):
return self.section.get("password", None)
@property
def nick(self):
return self.section.get("nick", None)
class Bot(ClientXMPP): class Bot(ClientXMPP):
"""XMPP bots for humans."""
CONFIG_FILE = "bot.conf" CONFIG_FILE = "bot.conf"
def __init__(self): def __init__(self):
@ -56,22 +79,36 @@ class Bot(ClientXMPP):
def parse_arguments(self): def parse_arguments(self):
"""Parse command-line arguments.""" """Parse command-line arguments."""
self.parser = ArgumentParser() self.parser = ArgumentParser(description="XMPP bots for humans")
self.parser.add_argument(
"--input",
help="Read configuration from environment",
action=BooleanOptionalAction,
default=True,
)
self.parser.add_argument( self.parser.add_argument(
"-d", "-d",
"--debug", "--debug",
help="Set logging to DEBUG", help="Enable verbose debug logs",
action="store_const", action="store_const",
dest="log_level", dest="log_level",
const=DEBUG, const=DEBUG,
default=INFO, default=INFO,
) )
self.parser.add_argument(
"-a",
"--account",
dest="account",
help="Account for the bot account (foo@example.com)",
)
self.parser.add_argument(
"-p",
"--password",
dest="password",
help="Password for the bot account",
)
self.parser.add_argument(
"-n",
"--nick",
dest="nick",
help="Nickname for the bot account",
)
self.args = self.parser.parse_args() self.args = self.parser.parse_args()
def setup_logging(self): def setup_logging(self):
@ -83,31 +120,26 @@ class Bot(ClientXMPP):
def read_config(self): def read_config(self):
"""Read configuration for running bot.""" """Read configuration for running bot."""
self.config = ConfigParser() config = ConfigParser()
config_file_path = Path(self.CONFIG_FILE).absolute() config_file_path = Path(self.CONFIG_FILE).absolute()
if not exists(config_file_path) and self.args.input:
if not exists(config_file_path) and stdout.isatty():
self.generate_config_interactively() self.generate_config_interactively()
if exists(config_file_path): if exists(config_file_path):
self.config.read(config_file_path) config.read(config_file_path)
if self.args.input is False: self.config = Config(config)
self.read_config_from_env()
if "bot" not in self.config:
raise RuntimeError("Failed to configure bot")
def generate_config_interactively(self): def generate_config_interactively(self):
"""Generate bot configuration.""" """Generate bot configuration."""
jid = input("XMPP address (e.g. foo@bar.com): ") or "foo@bar.com" account = input("Account: ")
password = ( password = getpass("Password: ")
getpass("Password (e.g. my-cool-password): ") or "my-cool-password" nick = input("Nickname: ")
)
nick = input("Nickname (e.g. lurkbot): ")
config = ConfigParser() config = ConfigParser()
config["bot"] = {"jid": jid, "password": password} config["bot"] = {"account": account, "password": password}
if nick: if nick:
config["bot"]["nick"] = nick config["bot"]["nick"] = nick
@ -115,18 +147,37 @@ class Bot(ClientXMPP):
with open("bot.conf", "w") as file_handle: with open("bot.conf", "w") as file_handle:
config.write(file_handle) config.write(file_handle)
def read_config_from_env(self):
"""Read configuration from the environment."""
self.config["bot"] = {}
self.config["bot"]["jid"] = environ.get("XBOT_JID")
self.config["bot"]["password"] = environ.get("XBOT_PASSWORD")
self.config["bot"]["nick"] = environ.get("XBOT_NICK", "")
def init_bot(self): def init_bot(self):
"""Initialise bot with connection details.""" """Initialise bot with connection details."""
jid = self.config["bot"]["jid"] account = (
password = self.config["bot"]["password"] self.args.account
ClientXMPP.__init__(self, jid, password) or self.config.account
or environ.get("XBOT_ACCOUNT", None)
)
password = (
self.args.password
or self.config.password
or environ.get("XBOT_PASSWORD", None)
)
nick = (
self.args.nick or self.config.nick or environ.get("XBOT_NICK", None)
)
if not account:
self.log.error("Unable to discover account")
exit(1)
if not password:
self.log.error("Unable to discover password")
exit(1)
if not nick:
self.log.error("Unable to discover nick")
exit(1)
ClientXMPP.__init__(self, account, password)
self.account = account
self.password = password
self.nick = nick
def register_xmpp_event_handlers(self): def register_xmpp_event_handlers(self):
"""Register functions against specific XMPP event handlers.""" """Register functions against specific XMPP event handlers."""
@ -150,14 +201,12 @@ class Bot(ClientXMPP):
def group_invite(self, message): def group_invite(self, message):
"""Accept invites to group chats.""" """Accept invites to group chats."""
self.plugin["xep_0045"].join_muc( self.plugin["xep_0045"].join_muc(message["from"], self.config.nick)
message["from"], self.config["bot"]["nick"]
)
def group_message(self, message): def group_message(self, message):
"""Handle groupchat_message event.""" """Handle groupchat_message event."""
if message["type"] in ("groupchat", "normal"): if message["type"] in ("groupchat", "normal"):
if message["mucnick"] != self.config["bot"]["nick"]: if message["mucnick"] != self.config.nick:
try: try:
self.group(SimpleMessage(message)) self.group(SimpleMessage(message))
except AttributeError: except AttributeError: