diff --git a/.env.template b/.env.template index d8270ba..dcf593b 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,4 @@ -OWLBOT_SECRET= OP_START_CHANNEL_ID= GUILD_ID= INTERVIEWER_ROLE_ID= -WELCOME_CHANNEL_ID= \ No newline at end of file +WELCOME_CHANNEL_ID= diff --git a/.github/workflows/dev-image-build.yml b/.github/workflows/dev-image-build.yml new file mode 100644 index 0000000..1ec0a46 --- /dev/null +++ b/.github/workflows/dev-image-build.yml @@ -0,0 +1,32 @@ +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: 'discord-owlbot' + - + name: Build the Docker image + working-directory: discord-owlbot + run: docker build . --file 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..905f7c7 --- /dev/null +++ b/.github/workflows/stable-image-build.yml @@ -0,0 +1,29 @@ +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 + - + name: Build the Docker image + run: docker build . --file 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..1464ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # CNTO Custom log *.log +.env.local* +config.henrik-test-server +discord-token.txt # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1bba24a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM alpine:3.19 + +ADD . /owlbot +WORKDIR /owlbot + +RUN apk add --no-cache python3 py3-pip +RUN pip install --break-system-packages -r requirements.txt +RUN crontab crontab.txt + +# Install tzdata to switch timezone on deployment +RUN apk add --no-cache tzdata + +CMD ["crond", "-f"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..40fe1ab --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# 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. Two versions are currently maintained: `dev` for development environments and `latest` for production environments. + +### 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 two parameters to run. + + 1. Discord channel id to post event reminders, this makes use of `.env` file (find a template in the `.env.template` file) + 2. Discord bot token to authenticate, this is passed using [docker-compose secrets](https://docs.docker.com/compose/use-secrets/). A plain text file named `discord-token.txt` should be placed in the same directory as `docker-compose.yml`, reference `discord-token.txt.template` for an example. + +The `.env` 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 + +Start the container through `docker-compose` to ensure proper parameters and secrets loading. + +```bash +docker-compose up -d +``` + +Is the only command you need to get Owlbot up and running. + +### Updating 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 image build. Changing the `crontab.txt` file content and restarting the container or `docker-compose` execution will have no impact on the actual cron entry installed. + +If you need to change the cron entry for debugging purposes without triggering an image rebuild, get access to a shell within the container (only `/bin/sh` since it's based on an `alpine` image) and edit the crontab manually, for example with `crontab -e`. \ No newline at end of file diff --git a/config b/config new file mode 100644 index 0000000..cb55c67 --- /dev/null +++ b/config @@ -0,0 +1,4 @@ +NOTIFICATION_CHANNEL_ID=342739973923143680 +WELCOME_CHANNEL_ID=814513168772628540 +GUILD_ID=154907081256730624 +INTERVIEWER_ROLE_ID=331492014883602433 diff --git a/config.cnto-test-server b/config.cnto-test-server new file mode 100644 index 0000000..c98b968 --- /dev/null +++ b/config.cnto-test-server @@ -0,0 +1,4 @@ +NOTIFICATION_CHANNEL_ID=1101131950745464842 +WELCOME_CHANNEL_ID=1101087724007587930 +GUILD_ID=1101050893526388758 +INTERVIEWER_ROLE_ID=1101061472391536660 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..cbad28f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" +services: + owlbot: + container_name: owlbot + # restart: unless-stopped + env_file: ./config + image: cntoarma/owlbot:dev + secrets: + - discord_token + environment: + - "TZ=Europe/Stockholm" + +secrets: + discord_token: + file: ./discord-token.txt \ No newline at end of file diff --git a/op-reminder.sh b/op-reminder.sh deleted file mode 100755 index 481fc2e..0000000 --- a/op-reminder.sh +++ /dev/null @@ -1,3 +0,0 @@ -source ~/miniconda3/etc/profile.d/conda.sh -conda activate discord-owlbot -python /home/carpenoctem/discord-owlbot/op-start-reminder.py diff --git a/owlbot.py b/owlbot.py index 3a3adce..bbc2f92 100644 --- a/owlbot.py +++ b/owlbot.py @@ -1,17 +1,22 @@ -import datetime -import discord -from dotenv import load_dotenv import os +import sys +import discord -load_dotenv() +# Discord secret and configuration variables retrieval +BOT_SECRET = None +try: + with open('/run/secrets/discord_token', 'r') as f: + BOT_SECRET = f.read() +except FileNotFoundError: + print("No docker secret") + exit(1) -CHANNEL_ID = os.environ['OP_START_CHANNEL_ID'] -BOT_SECRET = os.environ['OWLBOT_SECRET'] +NOTIFICATION_CHANNEL_ID = os.environ['NOTIFICATION_CHANNEL_ID'] +WELCOME_CHANNEL_ID= os.environ['WELCOME_CHANNEL_ID'] GUILD_ID = os.environ['GUILD_ID'] INTERVIEWER_ROLE_ID= os.environ['INTERVIEWER_ROLE_ID'] -WELCOME_CHANNEL_ID= os.environ['WELCOME_CHANNEL_ID'] -CNTO_TIMEZONE = datetime.timezone(datetime.timedelta(hours=1),"CNTO_TIMEZONE") -class MyClient(discord.Client): + +class OwlbotMemberWelcome(discord.Client): def __init__(self, *args, **kwargs): super().__init__(*args,**kwargs) @@ -30,8 +35,29 @@ async def on_member_join(self, member): embed.set_footer(text=f'{member.joined_at.strftime('%a %d %b %Y, %I:%M%p')}') await guild.system_channel.send(embed = embed) + +class OwlbotOperationNotification(discord.Client): + async def on_ready(self): + channel = await self.fetch_channel(NOTIFICATION_CHANNEL_ID) + # TODO: remove embedded role ids and overall message to make it configurable + await channel.send("Tonight's mission will start soon. <@&220093887518081024> and <@&665323023699673108> grab a drink and join us!") + await self.close() + intents = discord.Intents.default() +intents.messages = True intents.message_content = True intents.members = True -client = MyClient(intents=intents) -client.run(BOT_SECRET) \ No newline at end of file + +if __name__ == '__main__': + print(BOT_SECRET) + if len(sys.argv) == 1: + print("Starting member welcome workflow") + client = OwlbotMemberWelcome(intents=intents) + elif len(sys.argv) == 2 and sys.argv[1] == "operation-notification": + print("Starting operation notification workflow") + client = OwlbotOperationNotification(intents=intents) + else: + print("Invalid parameter. Only 'operation-notification' is allowed.") + exit(1) + + client.run(BOT_SECRET)