Rig up first-class file system support

See https://git.vvvvvvaria.org/decentral1se/xbotlib/issues/3.
This commit is contained in:
Luke Murphy 2021-01-23 23:29:58 +01:00
parent 1ed422ae86
commit eecfc8cd38
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
3 changed files with 92 additions and 45 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.conf
*.egg-info/
*.json
*.pyc
.avatars
.coverage

View File

@ -36,7 +36,7 @@ you want to chat or just invite your bots for testing.
- [Using the `.conf` configuration file](#using-the--conf--configuration-file)
- [Using the command-line interface](#using-the-command-line-interface)
- [Using the environment](#using-the-environment)
- [Persistent storage](#persistent-storage)
- [Storage back-end](#storage-back-end)
- [File system](#file-system)
- [Redis key/value storage](#redis-key-value-storage)
- [Loading Plugins](#loading-plugins)
@ -255,46 +255,35 @@ deployments.
- **XBOT_STORAGE**: choice of storage back-end (default: `file`)
- **XBOT_STORAGE_FILE**: path to file based storage back-end (default: `<nick>.json`)
### Persistent storage
### Storage back-end
#### File system
Just use your local file system as you would in any other Python script. Please
note that when you deploy your bot, you might not have access to this local
filesystem in the same location. For remote server deployments
[Redis](#redis-key-value-storage) can be more convenient.
#### Redis key/value storage
`xbotlib` supports using [Redis](https://redis.io/) as a storage back-end. It
is simple to work with because the interface is exactly like a dictionary. You
can quickly run Redis locally using [Docker](https://docs.docker.com/engine/install/debian/)
(`docker run --network=host --name redis -d redis`) or if you're on a Debian system you can
also `sudo apt install -y redis`.
You can configure the connection URL using the command-line interface,
configuration or environment. Here is an example using the environment.
```bash
$ export XBOT_REDIS_URL=redis://localhost:6379/0
```
And you access the interface via the `self.db` attribute.
In order to store data you can make use of the `self.db` attribute of the `Bot`
class. It is a Python dictionary which will be saved to disk automatically for
you as a `<nick>.json` in your current working directory. The name and path to
this file can be configured.
```python
def direct(self, message):
self.db["mykey"] = message.text
def group(self, message):
if not message.room in self.db.keys():
self.db[message.room] = "visited"
```
You should see `INFO Successfully connected to storage` when your bot
initialises. Please see the
[redis-py](https://redis-py.readthedocs.io/en/stable/) API documentation for
more.
If you want to inspect the database when the bot is not running, you can look
in the file directly.
```bash
$ cat mybot.json
```
For more advanced use cases, `xbotlib` also supports [Redis](https://redis.io/)
as a storage back-end. You'll need to configure this (e.g. `--storage redis`)
as the default uses the filesystem approach mentioned above. The same `self.db`
will then be passed as a Redis connection object.
### Loading Plugins
You can specify a `plugins = [...]` on your bot definition and they will be
automatically loaded.
automatically loaded when you start your bot.
```python
class MyBot(Bot):

View File

@ -7,6 +7,7 @@ from datetime import datetime as dt
from getpass import getpass
from imghdr import what
from inspect import cleandoc
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import environ
from os.path import exists
@ -155,6 +156,16 @@ class Config:
"""Turn on the web server."""
return self.section.get("serve", None)
@property
def storage(self):
"""Choice of storage back-end."""
return self.section.get("storage", None)
@property
def storage_file(self):
"""Path to the file based storage back-end."""
return self.section.get("storage_file", None)
class Bot(ClientXMPP):
"""XMPP bots for humans."""
@ -174,7 +185,7 @@ class Bot(ClientXMPP):
self.init_bot()
self.register_xmpp_event_handlers()
self.register_xmpp_plugins()
self.init_db()
self.init_storage()
self.run()
def parse_arguments(self):
@ -252,6 +263,19 @@ class Bot(ClientXMPP):
dest="serve",
help="turn on the web server",
)
self.parser.add_argument(
"-st",
"--storage",
dest="storage",
help="choice of storage back-end",
choices=("file", "redis"),
)
self.parser.add_argument(
"-stf",
"--storage-file",
dest="storage_file",
help="path to file based storage back-end",
)
self.args = self.parser.parse_args()
@ -377,6 +401,18 @@ class Bot(ClientXMPP):
or self.config.serve
or environ.get("XBOT_SERVE", None)
)
storage = (
self.args.storage
or self.config.storage
or environ.get("XBOT_STORAGE", None)
or "file"
)
storage_file = (
self.args.storage_file
or self.config.storage_file
or environ.get("XBOT_STORAGE_FILE", None)
or f"{nick}.json"
)
if not account:
self.log.error("Unable to discover account")
@ -400,6 +436,8 @@ class Bot(ClientXMPP):
self.port = port
self.template = self.load_template(template)
self.serve = serve
self.storage = storage
self.storage_file = Path(storage_file).absolute()
def load_template(self, template):
"""Load template via Jinja."""
@ -526,17 +564,26 @@ class Bot(ClientXMPP):
except AttributeError:
self.log.info("No additional plugins loaded")
def init_db(self):
"""Initialise the Redis key/value store."""
if not self.redis_url:
self.db = None
return self.log.info("No Redis storage discovered")
try:
self.db = Redis.from_url(self.redis_url, decode_responses=True)
self.log.info("Successfully connected to Redis storage")
except ValueError:
self.log.info("Failed to connect to Redis storage")
def init_storage(self):
"""Initialise the storage back-end."""
if self.storage == "file":
try:
self.db = {}
if exists(self.storage_file):
self.db = loads(open(self.storage_file, "r").read())
self.log.info("Successfully loaded file storage")
except Exception as exception:
message = f"Failed to load {self.storage_file}: {exception}"
self.log.info(message)
exit(1)
else:
try:
self.db = Redis.from_url(self.redis_url, decode_responses=True)
return self.log.info("Successfully connected to Redis storage")
except ValueError as exception:
message = f"Failed to connect to Redis storage: {exception}"
self.log.info(message)
exit(1)
def run(self):
"""Run the bot."""
@ -548,7 +595,17 @@ class Bot(ClientXMPP):
self.serve_web()
self.process(forever=False)
except (KeyboardInterrupt, RuntimeError):
pass
if self.storage != "file":
exit(0)
try:
with open(self.storage_file, "w") as handle:
handle.write(dumps(self.db))
self.log.info("Successfully saved file storage")
except Exception as exception:
message = f"Failed to save file storage: {exception}"
self.log.info(message)
exit(1)
def serve_web(self):
"""Serve the web."""