Add support for a configuration file.
This commit is contained in:
parent
6794f0ab03
commit
f21fd968fd
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,4 +3,6 @@
|
|||||||
.idea/
|
.idea/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
*.pyc
|
*.pyc
|
||||||
|
pluralkit.conf
|
||||||
|
pluralkit.*.conf
|
23
README.md
23
README.md
@ -8,16 +8,14 @@ PluralKit has a Discord server for support and discussion: https://discord.gg/Pc
|
|||||||
Running the bot requires Python (specifically version 3.6) and PostgreSQL.
|
Running the bot requires Python (specifically version 3.6) and PostgreSQL.
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
Configuring the bot is done through environment variables.
|
Configuring the bot is done through a configuration file. An example of the configuration format can be seen in [`pluralkit.conf.example`](https://github.com/xSke/PluralKit/blob/master/pluralkit.conf.example).
|
||||||
|
|
||||||
* TOKEN - the Discord bot token to connect with
|
The following keys are available:
|
||||||
* CLIENT_ID - the Discord bot client ID
|
* `token`: the Discord bot token to connect with
|
||||||
* DATABASE_USER - the username to log into the database with
|
* `database_uri`: the URI of the database to connect to (format: `postgres://username:password@hostname:port/database_name`)
|
||||||
* DATABASE_PASS - the password to log into the database with
|
* `log_channel` (optional): a Discord channel ID the bot will post exception tracebacks in (make this private!)
|
||||||
* DATABASE_NAME - the name of the database to use
|
|
||||||
* DATABASE_HOST - the hostname of the PostgreSQL instance to connect to
|
The environment variables `TOKEN` and `DATABASE_URI` will override the configuration file values when present.
|
||||||
* DATABASE_PORT - the port of the PostgreSQL instance to connect to
|
|
||||||
* LOG_CHANNEL (optional) - a Discord channel ID the bot will post exception tracebacks in (make this private!)
|
|
||||||
|
|
||||||
# Running
|
# Running
|
||||||
|
|
||||||
@ -25,17 +23,16 @@ Configuring the bot is done through environment variables.
|
|||||||
Running PluralKit is pretty easy with Docker. The repository contains a `docker-compose.yml` file ready to use.
|
Running PluralKit is pretty easy with Docker. The repository contains a `docker-compose.yml` file ready to use.
|
||||||
|
|
||||||
* Clone this repository: `git clone https://github.com/xSke/PluralKit`
|
* Clone this repository: `git clone https://github.com/xSke/PluralKit`
|
||||||
* Create a `.env` file containing at least `TOKEN` and `CLIENT_ID` in `key=value` format
|
* Create a `pluralkit.conf` file in the same directory as `docker-compose.yml` containing at least a `token` field
|
||||||
* Build the bot: `docker-compose build`
|
* Build the bot: `docker-compose build`
|
||||||
* Run the bot: `docker-compose up`
|
* Run the bot: `docker-compose up`
|
||||||
|
|
||||||
## Manually
|
## Manually
|
||||||
You'll need to pass configuration options through shell environment variables.
|
|
||||||
|
|
||||||
* Clone this repository: `git clone https://github.com/xSke/PluralKit`
|
* Clone this repository: `git clone https://github.com/xSke/PluralKit`
|
||||||
* Create a virtualenv: `virtualenv --python=python3.6 venv`
|
* Create a virtualenv: `virtualenv --python=python3.6 venv`
|
||||||
* Install dependencies: `venv/bin/pip install -r requirements.txt`
|
* Install dependencies: `venv/bin/pip install -r requirements.txt`
|
||||||
* Run PluralKit with environment variables: `TOKEN=... CLIENT_ID=... DATABASE_USER=... venv/bin/python src/bot_main.py`
|
* Run PluralKit with the config file: `venv/bin/python src/bot_main.py`
|
||||||
|
* The bot optionally takes a parameter describing the location of the configuration file, defaulting to `./pluralkit.conf`.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
This project is under the Apache License, Version 2.0. It is available at the following link: https://www.apache.org/licenses/LICENSE-2.0
|
This project is under the Apache License, Version 2.0. It is available at the following link: https://www.apache.org/licenses/LICENSE-2.0
|
@ -5,18 +5,12 @@ services:
|
|||||||
entrypoint:
|
entrypoint:
|
||||||
- python
|
- python
|
||||||
- bot_main.py
|
- bot_main.py
|
||||||
|
volumes:
|
||||||
|
- "./pluralkit.conf:/app/pluralkit.conf:ro"
|
||||||
|
environment:
|
||||||
|
- "DATABASE_URI=postgres://postgres:postgres@db:5432/postgres"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
environment:
|
|
||||||
- CLIENT_ID
|
|
||||||
- TOKEN
|
|
||||||
- LOG_CHANNEL
|
|
||||||
- TUPPERWARE_ID
|
|
||||||
- "DATABASE_USER=postgres"
|
|
||||||
- "DATABASE_PASS=postgres"
|
|
||||||
- "DATABASE_NAME=postgres"
|
|
||||||
- "DATABASE_HOST=db"
|
|
||||||
- "DATABASE_PORT=5432"
|
|
||||||
restart: always
|
restart: always
|
||||||
api:
|
api:
|
||||||
build: src/
|
build: src/
|
||||||
@ -29,11 +23,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "2939:8080"
|
- "2939:8080"
|
||||||
environment:
|
environment:
|
||||||
- "DATABASE_USER=postgres"
|
- "DATABASE_URI=postgres://postgres:postgres@db:5432/postgres"
|
||||||
- "DATABASE_PASS=postgres"
|
|
||||||
- "DATABASE_NAME=postgres"
|
|
||||||
- "DATABASE_HOST=db"
|
|
||||||
- "DATABASE_PORT=5432"
|
|
||||||
db:
|
db:
|
||||||
image: postgres:alpine
|
image: postgres:alpine
|
||||||
volumes:
|
volumes:
|
||||||
|
5
pluralkit.conf.example
Normal file
5
pluralkit.conf.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"database_uri": "postgres://username:password@hostname:port/database_name",
|
||||||
|
"token": "BOT_TOKEN_GOES_HERE",
|
||||||
|
"log_channel": null
|
||||||
|
}
|
@ -194,11 +194,7 @@ app.add_routes([
|
|||||||
|
|
||||||
async def run():
|
async def run():
|
||||||
app["pool"] = await db.connect(
|
app["pool"] = await db.connect(
|
||||||
os.environ["DATABASE_USER"],
|
os.environ["DATABASE_URI"]
|
||||||
os.environ["DATABASE_PASS"],
|
|
||||||
os.environ["DATABASE_NAME"],
|
|
||||||
os.environ["DATABASE_HOST"],
|
|
||||||
int(os.environ["DATABASE_PORT"])
|
|
||||||
)
|
)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# uvloop doesn't work on Windows, therefore an optional dependency
|
# uvloop doesn't work on Windows, therefore an optional dependency
|
||||||
@ -7,5 +10,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
from pluralkit import bot
|
with open(sys.argv[1] if len(sys.argv) > 1 else "pluralkit.conf") as f:
|
||||||
bot.run()
|
config = json.load(f)
|
||||||
|
|
||||||
|
if "database_uri" not in config and "database_uri" not in os.environ:
|
||||||
|
print("Config file must contain key 'database_uri', or the environment variable DATABASE_URI must be present.")
|
||||||
|
elif "token" not in config and "token" not in os.environ:
|
||||||
|
print("Config file must contain key 'token', or the environment variable TOKEN must be present.")
|
||||||
|
else:
|
||||||
|
from pluralkit import bot
|
||||||
|
bot.run(os.environ.get("TOKEN", config["token"]), os.environ.get("DATABASE_URI", config["database_uri"]), int(config.get("log_channel", 0)))
|
@ -13,36 +13,12 @@ from pluralkit.bot import commands, proxy, channel_logger, embeds
|
|||||||
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s")
|
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
|
||||||
def connect_to_database() -> asyncpg.pool.Pool:
|
def connect_to_database(uri: str) -> asyncpg.pool.Pool:
|
||||||
username = os.environ["DATABASE_USER"]
|
return asyncio.get_event_loop().run_until_complete(db.connect(uri))
|
||||||
password = os.environ["DATABASE_PASS"]
|
|
||||||
name = os.environ["DATABASE_NAME"]
|
|
||||||
host = os.environ["DATABASE_HOST"]
|
|
||||||
port = os.environ["DATABASE_PORT"]
|
|
||||||
|
|
||||||
if username is None or password is None or name is None or host is None or port is None:
|
|
||||||
print(
|
|
||||||
"Database credentials not specified. Please pass valid PostgreSQL database credentials in the DATABASE_[USER|PASS|NAME|HOST|PORT] environment variable.",
|
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = int(port)
|
|
||||||
except ValueError:
|
|
||||||
print("Please pass a valid integer as the DATABASE_PORT environment variable.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return asyncio.get_event_loop().run_until_complete(db.connect(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
database=name,
|
|
||||||
host=host,
|
|
||||||
port=port
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run(token: str, db_uri: str, log_channel_id: int):
|
||||||
pool = connect_to_database()
|
pool = connect_to_database(db_uri)
|
||||||
|
|
||||||
async def create_tables():
|
async def create_tables():
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
@ -51,7 +27,6 @@ def run():
|
|||||||
asyncio.get_event_loop().run_until_complete(create_tables())
|
asyncio.get_event_loop().run_until_complete(create_tables())
|
||||||
|
|
||||||
client = discord.Client()
|
client = discord.Client()
|
||||||
|
|
||||||
logger = channel_logger.ChannelLogger(client)
|
logger = channel_logger.ChannelLogger(client)
|
||||||
|
|
||||||
@client.event
|
@client.event
|
||||||
@ -98,11 +73,14 @@ def run():
|
|||||||
|
|
||||||
@client.event
|
@client.event
|
||||||
async def on_error(event_name, *args, **kwargs):
|
async def on_error(event_name, *args, **kwargs):
|
||||||
log_channel_id = os.environ.get("LOG_CHANNEL")
|
# Print it to stderr
|
||||||
|
logging.getLogger("pluralkit").exception("Exception while handling event {}".format(event_name))
|
||||||
|
|
||||||
|
# Then log it to the given log channel
|
||||||
|
# TODO: replace this with Sentry or something
|
||||||
if not log_channel_id:
|
if not log_channel_id:
|
||||||
return
|
return
|
||||||
|
log_channel = client.get_channel(log_channel_id)
|
||||||
log_channel = client.get_channel(int(log_channel_id))
|
|
||||||
|
|
||||||
# If this is a message event, we can attach additional information in an event
|
# If this is a message event, we can attach additional information in an event
|
||||||
# ie. username, channel, content, etc
|
# ie. username, channel, content, etc
|
||||||
@ -124,14 +102,4 @@ def run():
|
|||||||
if len(traceback.format_exc()) >= (2000 - len("```python\n```")):
|
if len(traceback.format_exc()) >= (2000 - len("```python\n```")):
|
||||||
traceback_str = "```python\n...{}```".format(traceback.format_exc()[- (2000 - len("```python\n...```")):])
|
traceback_str = "```python\n...{}```".format(traceback.format_exc()[- (2000 - len("```python\n...```")):])
|
||||||
await log_channel.send(content=traceback_str, embed=embed)
|
await log_channel.send(content=traceback_str, embed=embed)
|
||||||
|
client.run(token)
|
||||||
# Print it to stderr anyway, though
|
|
||||||
logging.getLogger("pluralkit").exception("Exception while handling event {}".format(event_name))
|
|
||||||
|
|
||||||
bot_token = os.environ["TOKEN"]
|
|
||||||
if not bot_token:
|
|
||||||
print("No token specified. Please pass a valid Discord bot token in the TOKEN environment variable.",
|
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
client.run(bot_token)
|
|
||||||
|
@ -22,7 +22,7 @@ async def help_root(ctx: CommandContext):
|
|||||||
|
|
||||||
|
|
||||||
async def invite_link(ctx: CommandContext):
|
async def invite_link(ctx: CommandContext):
|
||||||
client_id = os.environ["CLIENT_ID"]
|
client_id = (await ctx.client.application_info()).id
|
||||||
|
|
||||||
permissions = discord.Permissions()
|
permissions = discord.Permissions()
|
||||||
|
|
||||||
|
@ -12,10 +12,10 @@ from pluralkit.system import System
|
|||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.db")
|
logger = logging.getLogger("pluralkit.db")
|
||||||
async def connect(username, password, database, host, port):
|
async def connect(uri):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return await asyncpg.create_pool(user=username, password=password, database=database, host=host, port=port)
|
return await asyncpg.create_pool(uri)
|
||||||
except (ConnectionError, asyncpg.exceptions.CannotConnectNowError):
|
except (ConnectionError, asyncpg.exceptions.CannotConnectNowError):
|
||||||
logger.exception("Failed to connect to database, retrying in 5 seconds...")
|
logger.exception("Failed to connect to database, retrying in 5 seconds...")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
Loading…
Reference in New Issue
Block a user