Implement building with docker
See the README for usages and details. A breaking changes here too: * run.py is gone, we now handle that kind of thing with docker * The token is out of the config and is now an ENVAR (ideally using a .env) * `.json` files are now in a .matchy/ subdirectory, as this makes it a lot easier for the container to safely read Bonus: * fix a score_factors.setdefault call causing issues * Reformat some of the readme
This commit is contained in:
parent
b810dedb26
commit
3a0bf82ecb
10 changed files with 174 additions and 95 deletions
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
|
@ -0,0 +1 @@
|
|||
.gitignore
|
75
.github/workflows/publish.yml
vendored
Normal file
75
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
name: Create and publish a Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
pull_request:
|
||||
|
||||
# temp while iterating
|
||||
workflow_dispatch:
|
||||
|
||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
#
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=edge,branch=main
|
||||
|
||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,3 +3,5 @@ config.json
|
|||
state.json
|
||||
.venv
|
||||
.coverage
|
||||
.matchy
|
||||
.env
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -7,5 +7,6 @@
|
|||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"autopep8.interpreter": [".venv/bin/python"]
|
||||
"autopep8.interpreter": [".venv/bin/python"],
|
||||
"python.envFile": "${workspaceFolder}/.env",
|
||||
}
|
10
Dockerfile
Normal file
10
Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
FROM python:3.12-slim
|
||||
LABEL org.opencontainers.image.source=https://github.com/mdiluz/matchy
|
||||
LABEL org.opencontainers.image.description="Matchy matches matchees"
|
||||
LABEL org.opencontainers.image.licenses=Unlicense
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
CMD ["python", "py/matchy.py"]
|
84
README.md
84
README.md
|
@ -3,71 +3,52 @@ Matchy matches matchees.
|
|||
|
||||

|
||||
|
||||
Matchy is a Discord bot that groups up users for fun and vibes. Matchy can be installed on your server by clicking [here](https://discord.com/oauth2/authorize?client_id=1270849346987884696).
|
||||
Matchy is a Discord bot that groups up users for fun and vibes. Matchy can be installed on your server by clicking [here](https://discord.com/oauth2/authorize?client_id=1270849346987884696). Matchy only allows authorised users to trigger posts in channels.
|
||||
|
||||
## Commands
|
||||
Matchy is mostly managed with commands in server channels. These are all listed below.
|
||||
Matchy supports a bunch of user, `matcher` and bot owner commands. `/` commands are available in any channel the bot has access to, and `$` commands are only available in DMs.
|
||||
|
||||
### User commands
|
||||
#### /join and /leave
|
||||
Allows users to sign up and leave the group matching in the channel the command is used.
|
||||
|
||||
#### /pause [days: int(7)]
|
||||
Allows users to pause their matching in a channel for a given number of days. Users can use `/join` to re-join before the end of that time.
|
||||
|
||||
#### /list
|
||||
List the current matchees in the channel as well as any current scheduled match runs.
|
||||
|
||||
### Matcher commands
|
||||
Only usable by users with the `matcher` scope.
|
||||
|
||||
#### /match [group_min: int(3)]
|
||||
Matches groups of users in a channel and offers a button to pose those groups to the channel to users with `matcher` auth scope. Tracks historical matches and attempts to match users to make new connections with people with divergent roles, in an attempt to maximise diversity.
|
||||
|
||||
#### /schedule [group_min: int(3)] [weekday: int(0)] [hour: int(9)] [cancel: bool(False)]
|
||||
Allows a matcher to set a weekly schedule for matches in the channel, cancel can be used to remove a scheduled run
|
||||
|
||||
### Admin commands
|
||||
Only usable by users with the `owner` scope. Only usable in a DM with the bot user.
|
||||
|
||||
#### $sync and $close
|
||||
Syncs bot commands or closes down the bot.
|
||||
|
||||
#### $grant [user: int]
|
||||
Grant a given user the matcher scope to allow them to use `/match` and `/schedule`.
|
||||
| Command | Permissions | Description |
|
||||
|-----------|-------------|--------------------------------------------------------|
|
||||
| /join | user | Joins the matchee list |
|
||||
| /leave | user | Leaves the matchee list |
|
||||
| /pause | user | Pauses the user for `days: int` days |
|
||||
| /list | user | Lists the current matchees and scheduled matches |
|
||||
| /match | user | Shares a preview of the matchee groups of size `group_min: int` with the user, offers a button to post the match to `matcher` users |
|
||||
| /schedule | `matcher` | Shedules a match every week with `group_min: int` users on `weekday: int` day and at `hour: int` hour. Can pass `cancel: True` to stop the schedule |
|
||||
| $sync | bot owner | Syncs bot command data with the discord servers |
|
||||
| $close | bot owner | Closes the bot connection so the bot quits safely |
|
||||
| $grant | bot owner | Grants matcher to a given user (ID) |
|
||||
|
||||
## Development
|
||||
Current development is on Linux, though running on Mac or Windows should work fine.
|
||||
Development has been on on Linux so far, but running on Mac or Windows _should_ work fine. Please report any issues found.
|
||||
|
||||
### Dependencies
|
||||
* `python3` - Obviously, ideally 3.11
|
||||
* `venv` - Used for the python virtual env, specs in `requirements.txt`
|
||||
* `docker` - Optional, for deployment
|
||||
|
||||
### Getting Started
|
||||
```
|
||||
```bash
|
||||
git clone git@github.com:mdiluz/matchy.git
|
||||
cd matchy
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
git checkout -b [feature-branch-name]
|
||||
```
|
||||
VSCode can then be configured to use this new `.venv` and is the recommended way to develop.
|
||||
VSCode can be configured to use this new `.venv` and is the recommended way to develop.
|
||||
|
||||
### Tests
|
||||
Python tests are written to use `pytest` and cover most internal functionality. Tests can be run in the same way as in the Github Actions with [`scripts/test.py`](`scripts/test.py`), which lints all python code and runs any tests with `pytest`.
|
||||
|
||||
#### Coverage
|
||||
A helper script [`scripts/test-cov.py`](scripts/test-cov.py) is available to generate a html view on current code coverage.
|
||||
Python tests are written to use `pytest` and cover most internal functionality. Tests can be run in the same way as in the Github Actions with [`scripts/test.py`](`scripts/test.py`), which lints all python code and runs any tests with `pytest`. A helper script [`scripts/test-cov.py`](scripts/test-cov.py) is available to generate a html view on current code coverage.
|
||||
|
||||
## Hosting
|
||||
|
||||
### Config
|
||||
Matchy is configured by a required `config.json` file that takes this format:
|
||||
### Config and State
|
||||
Matchy is configured by an optional `$MATCHY_CONFIG` envar or a `.matchy/config.json` file that takes this format:
|
||||
```json
|
||||
{
|
||||
"version" : 1,
|
||||
"token" : "<<github bot token>>",
|
||||
|
||||
"version" : 2,
|
||||
"match" : {
|
||||
"score_factors": {
|
||||
"repeat_role" : 4,
|
||||
|
@ -78,22 +59,23 @@ Matchy is configured by a required `config.json` file that takes this format:
|
|||
}
|
||||
}
|
||||
```
|
||||
Only token and version are required. To generate bot token for development see [this discord.py guide](https://discordpy.readthedocs.io/en/stable/discord.html).
|
||||
Only the version is required.
|
||||
|
||||
See [`py/config.py`](py/config.py) for explanations for any extra settings here.
|
||||
|
||||
### Running
|
||||
It is recommended to only ever run the `release` branch in production, as this branch has passed the tests.
|
||||
_State_ is stored locally in a `.matchy/state.json` file. This will be created by the bot. This stores historical information on users, maching schedules, user auth scopes and more. See [`py/state.py`](py/state.py) for schema information if you need to inspect it.
|
||||
|
||||
Running the bot can be as simple as `python3 scripts/matchy.py`, but a [`scripts/run.py`](scripts/run.py) script is provided to update to the latest release, install any new `pip` dependencies and run the bot.
|
||||
### Secrets
|
||||
The `TOKEN` envar is required run the bot. It's recommended this is placed in a local `.env` file. To generate bot token for development see [this discord.py guide](https://discordpy.readthedocs.io/en/stable/discord.html).
|
||||
|
||||
The following command can be used to execute `run.py` on a loop, allowing the bot to be updated with a simple `$close` command from an `owner` user, but will still exit the loop if the bot throws a fatal error.
|
||||
### Docker
|
||||
Docker and Compose configs are provided, with the latest release tagged as `ghcr.io/mdiluz/matchy:latest`. A location for persistent data is stil required so some persistent volume will need to be mapped into the container as `/usr/share/app/.matchy`.
|
||||
|
||||
An example for how to do this may look like this:
|
||||
```bash
|
||||
docker run -v --env-file=.env ./.matchy:/usr/src/app/.matchy ghcr.io/mdiluz/matchy:latest
|
||||
```
|
||||
while ./scripts/run.py; end
|
||||
```
|
||||
|
||||
### State
|
||||
State is stored locally in a `state.json` file. This will be created by the bot. This stores historical information on users, maching schedules, user auth scopes and more. See [`py/state.py`](py/state.py) for schema information if you need to inspect it.
|
||||
A (`docker-compose.yml`)[docker-compose.yml] file is also provided that essentially performs the above when used with `docker compose up --exit-code-from matchy`. A `MATCHY_DATA` envar can be used in conjunction with compose to set a custom local path for the location of the data file.
|
||||
|
||||
## TODO
|
||||
* Implement better tests to the discordy parts of the codebase
|
||||
|
|
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
services:
|
||||
matchy:
|
||||
image: ghcr.io/mdiluz/matchy:latest
|
||||
env_file: ".env"
|
||||
volumes:
|
||||
- ${MATCHY_DATA:-./.matchy}:/usr/src/app/.matchy
|
53
py/config.py
53
py/config.py
|
@ -3,18 +3,20 @@ from schema import Schema, Use, Optional
|
|||
import files
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("config")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
_FILE = "config.json"
|
||||
# Envar takes precedent
|
||||
_ENVAR = "MATCHY_CONFIG"
|
||||
_FILE = ".matchy/config.json"
|
||||
|
||||
# Warning: Changing any of the below needs proper thought to ensure backwards compatibility
|
||||
_VERSION = 1
|
||||
_VERSION = 2
|
||||
|
||||
|
||||
class _Key():
|
||||
TOKEN = "token"
|
||||
VERSION = "version"
|
||||
|
||||
MATCH = "match"
|
||||
|
@ -27,6 +29,7 @@ class _Key():
|
|||
|
||||
# Removed
|
||||
_OWNERS = "owners"
|
||||
_TOKEN = "token"
|
||||
|
||||
|
||||
_SCHEMA = Schema(
|
||||
|
@ -34,9 +37,6 @@ _SCHEMA = Schema(
|
|||
# The current version
|
||||
_Key.VERSION: Use(int),
|
||||
|
||||
# Discord bot token
|
||||
_Key.TOKEN: Use(str),
|
||||
|
||||
# Settings for the match algorithmn, see matching.py for explanations on usage
|
||||
Optional(_Key.MATCH): {
|
||||
Optional(_Key.SCORE_FACTORS): {
|
||||
|
@ -51,7 +51,6 @@ _SCHEMA = Schema(
|
|||
)
|
||||
|
||||
_EMPTY_DICT = {
|
||||
_Key.TOKEN: "",
|
||||
_Key.VERSION: _VERSION
|
||||
}
|
||||
|
||||
|
@ -59,15 +58,23 @@ _EMPTY_DICT = {
|
|||
def _migrate_to_v1(d: dict):
|
||||
# Owners moved to History in v1
|
||||
# Note: owners will be required to be re-added to the state.json
|
||||
if _Key._OWNERS in d:
|
||||
owners = d.pop(_Key._OWNERS)
|
||||
logger.warning(
|
||||
"Migration removed owners from config, these must be re-added to the state.json")
|
||||
logger.warning("Owners: %s", owners)
|
||||
|
||||
|
||||
def _migrate_to_v2(d: dict):
|
||||
# Token moved to the environment
|
||||
if _Key._TOKEN in d:
|
||||
del d[_Key._TOKEN]
|
||||
|
||||
|
||||
# Set of migration functions to apply
|
||||
_MIGRATIONS = [
|
||||
_migrate_to_v1
|
||||
_migrate_to_v1,
|
||||
_migrate_to_v2
|
||||
]
|
||||
|
||||
|
||||
|
@ -105,7 +112,7 @@ class _Config():
|
|||
|
||||
@property
|
||||
def score_factors(self) -> _ScoreFactors:
|
||||
return _ScoreFactors(self._dict.setdefault(_Key.SCORE_FACTORS, {}))
|
||||
return _ScoreFactors(self._dict.get(_Key.SCORE_FACTORS, {}))
|
||||
|
||||
|
||||
def _migrate(dict: dict):
|
||||
|
@ -116,20 +123,30 @@ def _migrate(dict: dict):
|
|||
dict["version"] = _VERSION
|
||||
|
||||
|
||||
def _load_from_file(file: str = _FILE) -> _Config:
|
||||
def _load() -> _Config:
|
||||
"""
|
||||
Load the state from a file
|
||||
Load the state from an envar or file
|
||||
Apply any required migrations
|
||||
"""
|
||||
loaded = _EMPTY_DICT
|
||||
if os.path.isfile(file):
|
||||
loaded = files.load(file)
|
||||
_migrate(loaded)
|
||||
|
||||
# Try the envar first
|
||||
envar = os.environ.get(_ENVAR)
|
||||
if envar:
|
||||
loaded = json.loads(envar)
|
||||
logger.info("Config loaded from $%s", _ENVAR)
|
||||
else:
|
||||
logger.warning("No %s file found, bot cannot run!", file)
|
||||
# Otherwise try the file
|
||||
if os.path.isfile(_FILE):
|
||||
loaded = files.load(_FILE)
|
||||
logger.info("Config loaded from %s", _FILE)
|
||||
else:
|
||||
loaded = _EMPTY_DICT
|
||||
logger.warning("No %s file found, using defaults", _FILE)
|
||||
|
||||
_migrate(loaded)
|
||||
return _Config(loaded)
|
||||
|
||||
|
||||
# Core config for users to use
|
||||
# Singleton as there should only be one, and it's global
|
||||
Config = _load_from_file()
|
||||
# Singleton as there should only be one, it's static, and global
|
||||
Config = _load()
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import config
|
||||
import os
|
||||
from state import load_from_file
|
||||
from cogs.matchy_cog import MatchyCog
|
||||
from cogs.owner_cog import OwnerCog
|
||||
|
||||
_STATE_FILE = "state.json"
|
||||
_STATE_FILE = ".matchy/state.json"
|
||||
state = load_from_file(_STATE_FILE)
|
||||
|
||||
logger = logging.getLogger("matchy")
|
||||
|
@ -35,4 +35,6 @@ async def on_ready():
|
|||
|
||||
if __name__ == "__main__":
|
||||
handler = logging.StreamHandler()
|
||||
bot.run(config.Config.token, log_handler=handler, root_logger=True)
|
||||
token = os.environ.get("TOKEN", None)
|
||||
assert token, "$TOKEN required"
|
||||
bot.run(token, log_handler=handler, root_logger=True)
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import sys
|
||||
import git
|
||||
import subprocess
|
||||
|
||||
# Pull the release branch
|
||||
repo = git.Repo(search_parent_directories=True)
|
||||
if repo.active_branch.name != "release":
|
||||
print(f"Refusing to run on branch '{repo.active_branch.name}'")
|
||||
sys.exit(1)
|
||||
repo.remotes.origin.pull()
|
||||
|
||||
# Install any new pip requirements
|
||||
subprocess.run([sys.executable, "-m", "pip", "install",
|
||||
"-r", "requirements.txt"], check=True)
|
||||
|
||||
# Run Matchy!
|
||||
subprocess.run([sys.executable, "py/matchy.py"], check=True)
|
Loading…
Add table
Reference in a new issue