diff --git a/CHANGELOG.md b/CHANGELOG.md index 613884e..97d0343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ # xbotlib 0.8.0 (2021-01-14) - 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) diff --git a/README.md b/README.md index d57fb90..7061acf 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ $ pip install xbotlib ## Example -Put the following in a `echo.py` file. `xbotlib` provides a number of example -bots which you can use to get moving fast and try things out. +Put the following in a `echo.py` file. + +`xbotlib` provides a number of example bots which you can use to get moving +fast and try things out. ```python from xbotlib import EchoBot @@ -31,10 +33,10 @@ from xbotlib import EchoBot EchotBot() ``` -And then `python echo.py`. You will be asked a few questions like which account -details your bot will be using. - -This will generate a `bot.conf` file in the same working directory for further use. +And then `python echo.py`. You will be asked a few questions in order to load +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 +[configuration](#configure-your-bot) section for more. Here's the code for the `EchoBot`. @@ -95,15 +97,29 @@ Attributes: ## 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 -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_JID**: The username of the bot account -- **XBOT_PASSWORD**: The password of the bot account -- **XBOT_NICK**: The nickname that the bot uses +- **XBOT_ACCOUNT**: The bot account +- **XBOT_PASSWORD**: The bot password +- **XBOT_NICK**: The bot nickname ## Roadmap diff --git a/xbotlib.py b/xbotlib.py index 546214d..677cddd 100644 --- a/xbotlib.py +++ b/xbotlib.py @@ -1,12 +1,13 @@ """XMPP bots for humans.""" -from argparse import ArgumentParser, BooleanOptionalAction +from argparse import ArgumentParser from configparser import ConfigParser from getpass import getpass from logging import DEBUG, INFO, basicConfig, getLogger from os import environ from os.path import exists from pathlib import Path +from sys import exit, stdout from slixmpp import ClientXMPP @@ -42,7 +43,29 @@ class SimpleMessage: 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): + """XMPP bots for humans.""" + CONFIG_FILE = "bot.conf" def __init__(self): @@ -56,22 +79,36 @@ class Bot(ClientXMPP): def parse_arguments(self): """Parse command-line arguments.""" - self.parser = ArgumentParser() - self.parser.add_argument( - "--input", - help="Read configuration from environment", - action=BooleanOptionalAction, - default=True, - ) + self.parser = ArgumentParser(description="XMPP bots for humans") + self.parser.add_argument( "-d", "--debug", - help="Set logging to DEBUG", + help="Enable verbose debug logs", action="store_const", dest="log_level", const=DEBUG, 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() def setup_logging(self): @@ -83,31 +120,26 @@ class Bot(ClientXMPP): def read_config(self): """Read configuration for running bot.""" - self.config = ConfigParser() + config = ConfigParser() 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() if exists(config_file_path): - self.config.read(config_file_path) + config.read(config_file_path) - if self.args.input is False: - self.read_config_from_env() - - if "bot" not in self.config: - raise RuntimeError("Failed to configure bot") + self.config = Config(config) def generate_config_interactively(self): """Generate bot configuration.""" - jid = input("XMPP address (e.g. foo@bar.com): ") or "foo@bar.com" - password = ( - getpass("Password (e.g. my-cool-password): ") or "my-cool-password" - ) - nick = input("Nickname (e.g. lurkbot): ") + account = input("Account: ") + password = getpass("Password: ") + nick = input("Nickname: ") config = ConfigParser() - config["bot"] = {"jid": jid, "password": password} + config["bot"] = {"account": account, "password": password} if nick: config["bot"]["nick"] = nick @@ -115,18 +147,37 @@ class Bot(ClientXMPP): with open("bot.conf", "w") as 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): """Initialise bot with connection details.""" - jid = self.config["bot"]["jid"] - password = self.config["bot"]["password"] - ClientXMPP.__init__(self, jid, password) + account = ( + self.args.account + 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): """Register functions against specific XMPP event handlers.""" @@ -150,14 +201,12 @@ class Bot(ClientXMPP): def group_invite(self, message): """Accept invites to group chats.""" - self.plugin["xep_0045"].join_muc( - message["from"], self.config["bot"]["nick"] - ) + self.plugin["xep_0045"].join_muc(message["from"], self.config.nick) def group_message(self, message): """Handle groupchat_message event.""" if message["type"] in ("groupchat", "normal"): - if message["mucnick"] != self.config["bot"]["nick"]: + if message["mucnick"] != self.config.nick: try: self.group(SimpleMessage(message)) except AttributeError: