diff --git a/.env.template b/.env.template index 2e4c988..2288cb9 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1 @@ -OWLBOT_SECRET= -OP_START_CHANNEL_ID= +OP_START_CHANNEL_ID= \ No newline at end of file diff --git a/.github/workflows/dev-image-build.yml b/.github/workflows/dev-image-build.yml new file mode 100644 index 0000000..0811ac0 --- /dev/null +++ b/.github/workflows/dev-image-build.yml @@ -0,0 +1,31 @@ +name: Build Owlbot DEV Docker image + +on: + push: + branches: [ "dockerize", "dev" ] + +jobs: + + build: + + runs-on: ubuntu-latest + environment: dev + + steps: + - + name: Checkout repository + uses: actions/checkout@v4 + with: + path: 'owlbot-repo' + - + name: Build the Docker image + run: docker build . --file owlbot-repo/Dockerfile --tag cntoarma/owlbot:dev + - + name: Authenticate to CNTO DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Publish image to DockerHub + run: docker image push cntoarma/owlbot:dev diff --git a/.github/workflows/stable-image-build.yml b/.github/workflows/stable-image-build.yml new file mode 100644 index 0000000..bd22f2a --- /dev/null +++ b/.github/workflows/stable-image-build.yml @@ -0,0 +1,31 @@ +name: Build Owlbot Docker image + +on: + push: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + environment: stable + + steps: + - + name: Checkout repository + uses: actions/checkout@v4 + with: + path: 'owlbot-repo' + - + name: Build the Docker image + run: docker build . --file owlbot-repo/Dockerfile --tag cntoarma/owlbot:latest + - + name: Authenticate to CNTO DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Publish image to DockerHub + run: docker image push cntoarma/owlbot:latest diff --git a/.gitignore b/.gitignore index ee95d32..e537967 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # CNTO Custom log *.log +.env.local* +discord-token.txt # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6465832 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine:3.19 + +ADD owlbot-repo /owlbot +WORKDIR /owlbot + +RUN apk add --no-cache python3 py3-pip +RUN pip install --break-system-packages -r requirements.txt +RUN crontab crontab.txt + +CMD ["crond", "-f"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b27b1fa --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# CNTO Owlbot + +## What is the Owlbot + +Owlbot is CNTO's omnipresent assistant. It is currently implemented as a TeamSpeak user for populating our `stats` pages, and on Discord for automated member pings before our operations begin. R&D has plans to expand the Discord presence to enable staff members to carry out their tasks directly from our Discord, but this is a WIP. + +## Installation guide + +Owlbot is available as [a Docker image](https://hub.docker.com/repository/docker/cntoarma/owlbot/general), published on CNTO's DockerHub registry. + +### 1. Create a Discord application from the Discord Developer Portal + +You can follow [this guide](https://discordpy.readthedocs.io/en/stable/discord.html) to create a bot application. CNTO has two versions of the Owlbot managed by R&D: OWL and OWL Dev. The key takeaway from this step is to obtain the bot's secret, used to authenticate the bot against Discord's API. + +### 2. Gather the required parameters + +Within CNTO, the Owlbot is deployed on the `Tools Server`. It requires a few parameters as described in the `.env.template` file, namely: + +```language=config +OWLBOT_SECRET= # The bot's secret obtained on the Discord Developer Portal +OP_START_CHANNEL_ID= # The ID of the channel where operations reminders will be posted +``` + +These parameters can be passed to Owlbot in two different ways: + +1. As system environment variables, for example `export OWLBOT_SECRET=1234` +2. As `.env` file in the same directory as the `.env.template` + +Owlbot will try to load from a `.env` file, falling back to system variables. If both are present, system variables are used. + +**Note** if you use the Owlbot Docker image from our public registry, the `.env` file is not yet created by the build process so you need to rely on environment variables. The `.env` file mechanism is supported for local development environments. We do not plan on adding `.env` file support for staging / production environments. + +### 3. Run the Docker image + +Launch a container based on the Owlbot Docker image, for example: + +```bash +docker run -d -e OWLBOT_SECRET= -e OP_START_CHANNEL_ID= cntoarma/owlbot:[stable|dev] +``` + +If you still want to use a `.env` you can avoid passing environment variables to the run command, for example: + +```bash +docker run -d --env-file .env cntoarma/owlbot:[stable|dev] +``` + +**Note** using `docker-compose` is preferred in production environments as it can also handle container restarts, ensuring the Owlbot is operational all the time. + +#### Update the crontab file + +By default, the Owlbot will send join reminders 10 minutes before our events start (19:35 CET / CEST). This is configured in the `crontab.txt` file that gets installed inside the Owlbot container upon build. If you need to change the \ No newline at end of file diff --git a/crontab.txt b/crontab.txt new file mode 100644 index 0000000..1a9471a --- /dev/null +++ b/crontab.txt @@ -0,0 +1 @@ +35 19 * * 2,5 sh /owlbot/op-reminder.sh diff --git a/discord-token.txt.template b/discord-token.txt.template new file mode 100644 index 0000000..51e6ba0 --- /dev/null +++ b/discord-token.txt.template @@ -0,0 +1 @@ +DISCORD_TOKEN_HERE \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6a92082 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3" +services: + owlbot: + container_name: owlbot + # restart: unless-stopped + env_file: .env + image: cntoarma/owlbot:dev + secrets: + - discord_token + +secrets: + discord_token: + file: ./discord-token.txt \ No newline at end of file diff --git a/op-reminder.sh b/op-reminder.sh index 481fc2e..26f89af 100755 --- a/op-reminder.sh +++ b/op-reminder.sh @@ -1,3 +1 @@ -source ~/miniconda3/etc/profile.d/conda.sh -conda activate discord-owlbot -python /home/carpenoctem/discord-owlbot/op-start-reminder.py +python /owlbot/op-start-reminder.py > /owlbot/cron.log 2>&1 diff --git a/op-start-reminder.py b/op-start-reminder.py index 9aaf3cb..d6b8bc6 100644 --- a/op-start-reminder.py +++ b/op-start-reminder.py @@ -1,18 +1,29 @@ import discord -from dotenv import load_dotenv +from dotenv import dotenv_values import os -load_dotenv() +# Load from .env (for local dev environments) or override with environment variables (for production) +config = { + **dotenv_values('.env'), + **os.environ +} -CHANNEL_ID = os.enivon['OP_START_CHANNEL_ID'] -BOT_SECRET = os.environ['OWLBOT_SECRET'] +CHANNEL_ID = config['OP_START_CHANNEL_ID'] + +# Read bot token for Discord auth from docker-compose secrets +BOT_SECRET = None +with open('/run/secrets/discord_token', 'r') as f: + BOT_SECRET = f.read() + +if BOT_SECRET is None: + raise ValueError("Unable to read Discord token") class MyClient(discord.Client): async def on_ready(self): # async for guild in client.fetch_guilds(limit=150): # print(guild.name) - channel = self.get_channel(CHANNEL_ID) + channel = await self.fetch_channel(CHANNEL_ID) await channel.send("Tonight's OP is about to start, @here grab a drink and join us!") await self.close()