Skip to content

Commit

Permalink
feat(github): add github integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ReenigneArcher committed Dec 8, 2024
1 parent cb15726 commit 0fd9c62
Show file tree
Hide file tree
Showing 35 changed files with 2,111 additions and 377 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ jobs:
- name: Test with pytest
id: test
env:
CI_EVENT_ID: ${{ github.event.number || github.sha }}
GITHUB_PYTEST: "true"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_TEST_BOT_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_TEST_BOT_WEBHOOK }}
DISCORD_GITHUB_STATUS_CHANNEL_ID: ${{ vars.DISCORD_GITHUB_STATUS_CHANNEL_ID }}
DISCORD_REDDIT_CHANNEL_ID: ${{ vars.DISCORD_REDDIT_CHANNEL_ID }}
DISCORD_SPONSORS_CHANNEL_ID: ${{ vars.DISCORD_SPONSORS_CHANNEL_ID }}
GRAVATAR_EMAIL: ${{ secrets.GRAVATAR_EMAIL }}
IGDB_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
IGDB_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
PRAW_CLIENT_ID: ${{ secrets.REDDIT_CLIENT_ID }}
PRAW_CLIENT_SECRET: ${{ secrets.REDDIT_CLIENT_SECRET }}
REDDIT_USERNAME: ${{ secrets.REDDIT_USERNAME }}
Expand Down
29 changes: 22 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,50 @@ ENV COMMIT=${COMMIT}
ARG DAILY_TASKS=true
ARG DAILY_RELEASES=true
ARG DAILY_TASKS_UTC_HOUR=12
ARG DISCORD_GITHUB_STATUS_CHANNEL_ID
ARG DISCORD_REDDIT_CHANNEL_ID
ARG DISCORD_SPONSORS_CHANNEL_ID

# Secret config
ARG DISCORD_BOT_TOKEN
ARG DAILY_CHANNEL_ID
ARG DISCORD_BOT_TOKEN

Check warning on line 26 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "DISCORD_BOT_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG DISCORD_CLIENT_ID
ARG DISCORD_CLIENT_SECRET

Check warning on line 28 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "DISCORD_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG DISCORD_REDIRECT_URI
ARG GITHUB_CLIENT_ID
ARG GITHUB_CLIENT_SECRET

Check warning on line 31 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "GITHUB_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG GITHUB_REDIRECT_URI
ARG GITHUB_WEBHOOK_SECRET_KEY

Check warning on line 33 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "GITHUB_WEBHOOK_SECRET_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG GRAVATAR_EMAIL
ARG IGDB_CLIENT_ID
ARG IGDB_CLIENT_SECRET

Check warning on line 36 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "IGDB_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG PRAW_CLIENT_ID
ARG PRAW_CLIENT_SECRET
ARG PRAW_SUBREDDIT
ARG DISCORD_WEBHOOK
ARG GRAVATAR_EMAIL
ARG REDIRECT_URI

# Environment variables
ENV DAILY_TASKS=$DAILY_TASKS
ENV DAILY_RELEASES=$DAILY_RELEASES
ENV DAILY_CHANNEL_ID=$DAILY_CHANNEL_ID
ENV DAILY_TASKS_UTC_HOUR=$DAILY_TASKS_UTC_HOUR
ENV DISCORD_BOT_TOKEN=$DISCORD_BOT_TOKEN

Check warning on line 46 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DISCORD_BOT_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV DISCORD_CLIENT_ID=$DISCORD_CLIENT_ID
ENV DISCORD_CLIENT_SECRET=$DISCORD_CLIENT_SECRET

Check warning on line 48 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DISCORD_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV DISCORD_GITHUB_STATUS_CHANNEL_ID=$DISCORD_GITHUB_STATUS_CHANNEL_ID
ENV DISCORD_REDDIT_CHANNEL_ID=$DISCORD_REDDIT_CHANNEL_ID
ENV DISCORD_REDIRECT_URI=$DISCORD_REDIRECT_URI
ENV DISCORD_SPONSORS_CHANNEL_ID=$DISCORD_SPONSORS_CHANNEL_ID
ENV GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID
ENV GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET

Check warning on line 54 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "GITHUB_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV GITHUB_REDIRECT_URI=$GITHUB_REDIRECT_URI
ENV GITHUB_WEBHOOK_SECRET_KEY=$GITHUB_WEBHOOK_SECRET_KEY

Check warning on line 56 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "GITHUB_WEBHOOK_SECRET_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV GRAVATAR_EMAIL=$GRAVATAR_EMAIL
ENV IGDB_CLIENT_ID=$IGDB_CLIENT_ID
ENV IGDB_CLIENT_SECRET=$IGDB_CLIENT_SECRET
ENV PRAW_CLIENT_ID=$PRAW_CLIENT_ID
ENV PRAW_CLIENT_SECRET=$PRAW_CLIENT_SECRET
ENV PRAW_SUBREDDIT=$PRAW_SUBREDDIT
ENV DISCORD_WEBHOOK=$DISCORD_WEBHOOK

Check warning on line 63 in Dockerfile

View workflow job for this annotation

GitHub Actions / Docker

Variables should be defined before their use

UndefinedVar: Usage of undefined variable '$DISCORD_WEBHOOK' More info: https://docs.docker.com/go/dockerfile/rule/undefined-var/
ENV GRAVATAR_EMAIL=$GRAVATAR_EMAIL
ENV REDIRECT_URI=$REDIRECT_URI

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# install dependencies
Expand All @@ -69,7 +84,7 @@ RUN <<_SETUP
set -e

# replace the version in the code
sed -i "s/version = '0.0.0'/version = '${BUILD_VERSION}'/g" src/common.py
sed -i "s/version = '0.0.0'/version = '${BUILD_VERSION}'/g" src/common/common.py

# install dependencies
python -m pip install --no-cache-dir -r requirements.txt
Expand Down
55 changes: 33 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
[![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/support-bot/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/support-bot/actions/workflows/ci.yml?query=branch%3Amaster)
[![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/support-bot.svg?token=900Q93P1DE&style=for-the-badge&logo=codecov&label=codecov)](https://app.codecov.io/gh/LizardByte/support-bot)

Support bot written in python to help manage LizardByte communities. The current focus is discord and reddit, but other
platforms such as GitHub discussions/issues could be added.
Support bot written in python to help manage LizardByte communities. The current focus is Discord and Reddit, but other
platforms such as GitHub discussions/issues might be added in the future.


## Overview
Expand All @@ -28,23 +28,28 @@ platforms such as GitHub discussions/issues could be added.
:exclamation: if using Docker these can be arguments.
:warning: Never publicly expose your tokens, secrets, or ids.

| variable | required | default | description |
|-------------------------|----------|------------------------------------------------------|---------------------------------------------------------------|
| DISCORD_BOT_TOKEN | True | `None` | Token from Bot page on discord developer portal. |
| DAILY_TASKS | False | `true` | Daily tasks on or off. |
| DAILY_RELEASES | False | `true` | Send a message for each game released on this day in history. |
| DAILY_CHANNEL_ID | False | `None` | Required if daily_tasks is enabled. |
| DAILY_TASKS_UTC_HOUR | False | `12` | The hour to run daily tasks. |
| GRAVATAR_EMAIL | False | `None` | Gravatar email address for bot avatar. |
| IGDB_CLIENT_ID | False | `None` | Required if daily_releases is enabled. |
| IGDB_CLIENT_SECRET | False | `None` | Required if daily_releases is enabled. |
| SUPPORT_COMMANDS_REPO | False | `https://github.com/LizardByte/support-bot-commands` | Repository for support commands. |
| SUPPORT_COMMANDS_BRANCH | False | `master` | Branch for support commands. |

* Running bot:
* `python -m src`
* Invite bot to server:
* `https://discord.com/api/oauth2/authorize?client_id=<the client id of the bot>&permissions=8&scope=bot%20applications.commands`
| variable | required | default | description |
|----------------------------------|----------|------------------------------------------------------|---------------------------------------------------------------|
| DISCORD_BOT_TOKEN | True | `None` | Token from Bot page on discord developer portal. |
| DISCORD_CLIENT_ID | True | `None` | Discord OAuth2 client id. |
| DISCORD_CLIENT_SECRET | True | `None` | Discord OAuth2 client secret. |
| DISCORD_GITHUB_STATUS_CHANNEL_ID | True | `None` | Channel ID to send GitHub status updates to. |
| DISCORD_REDDIT_CHANNEL_ID | True | `None` | Channel ID to send Reddit post updates to. |
| DISCORD_REDIRECT_URI | False | `https://localhost:8080/discord/callback` | The redirect uri for OAuth2. Must be publicly accessible. |
| DISCORD_SPONSORS_CHANNEL_ID | True | `None` | Channel ID to send sponsorship updates to. |
| GITHUB_CLIENT_ID | True | `None` | GitHub OAuth2 client id. |
| GITHUB_CLIENT_SECRET | True | `None` | GitHub OAuth2 client secret. |
| GITHUB_REDIRECT_URI | False | `https://localhost:8080/github/callback` | The redirect uri for OAuth2. Must be publicly accessible. |
| GITHUB_WEBHOOK_SECRET_KEY | True | `None` | A secret value to ensure webhooks are from trusted sources. |
| DAILY_TASKS | False | `true` | Daily tasks on or off. |
| DAILY_RELEASES | False | `true` | Send a message for each game released on this day in history. |
| DAILY_CHANNEL_ID | False | `None` | Required if daily_tasks is enabled. |
| DAILY_TASKS_UTC_HOUR | False | `12` | The hour to run daily tasks. |
| GRAVATAR_EMAIL | False | `None` | Gravatar email address for bot avatar. |
| IGDB_CLIENT_ID | False | `None` | Required if daily_releases is enabled. |
| IGDB_CLIENT_SECRET | False | `None` | Required if daily_releases is enabled. |
| SUPPORT_COMMANDS_REPO | False | `https://github.com/LizardByte/support-bot-commands` | Repository for support commands. |
| SUPPORT_COMMANDS_BRANCH | False | `master` | Branch for support commands. |


### Reddit
Expand All @@ -62,7 +67,13 @@ platforms such as GitHub discussions/issues could be added.
| DISCORD_WEBHOOK | False | None | URL of webhook to send discord notifications to |
| GRAVATAR_EMAIL | False | None | Gravatar email address to get avatar from |
| REDDIT_USERNAME | True | None | Reddit username |
* | REDDIT_PASSWORD | True | None | Reddit password |
| REDDIT_PASSWORD | True | None | Reddit password |

### Start

* Running bot:
* `python -m src`
```bash
python -m src
```

* Invite bot to server:
* `https://discord.com/api/oauth2/authorize?client_id=<the client id of the bot>&permissions=8&scope=bot%20applications.commands`
Binary file added assets/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ betamax-serializers==0.2.1
pytest==8.3.3
pytest-asyncio==0.24.0
pytest-cov==6.0.0
pytest-mock==3.14.0
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cryptography==43.0.3
Flask==3.1.0
GitPython==3.1.43
igdb-api-v4==0.3.3
Expand All @@ -7,3 +8,4 @@ praw==7.8.1
py-cord==2.6.1
python-dotenv==1.0.1
requests==2.32.3
requests-oauthlib==2.0.0
33 changes: 13 additions & 20 deletions src/__main__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
# standard imports
import os
import time

# development imports
from dotenv import load_dotenv
load_dotenv(override=False) # environment secrets take priority over .env file

# local imports
if True: # hack for flake8
from src.discord import bot as d_bot
from src import keep_alive
from src.reddit import bot as r_bot
# local imports, import after env loaded
from src.common import globals # noqa: E402
from src.discord import bot as d_bot # noqa: E402
from src.common import webapp # noqa: E402
from src.reddit import bot as r_bot # noqa: E402

Check warning on line 12 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L9-L12

Added lines #L9 - L12 were not covered by tests


def main():
# to run in replit
try:
os.environ['REPL_SLUG']
except KeyError:
pass # not running in replit
else:
keep_alive.keep_alive() # Start the web server
webapp.start() # Start the web server

Check warning on line 16 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L16

Added line #L16 was not covered by tests

discord_bot = d_bot.Bot()
discord_bot.start_threaded() # Start the discord bot
globals.DISCORD_BOT = d_bot.Bot()
globals.DISCORD_BOT.start_threaded() # Start the discord bot

Check warning on line 19 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L18-L19

Added lines #L18 - L19 were not covered by tests

reddit_bot = r_bot.Bot()
reddit_bot.start_threaded() # Start the reddit bot
globals.REDDIT_BOT = r_bot.Bot()
globals.REDDIT_BOT.start_threaded() # Start the reddit bot

Check warning on line 22 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L21-L22

Added lines #L21 - L22 were not covered by tests

try:
while discord_bot.bot_thread.is_alive() or reddit_bot.bot_thread.is_alive():
while globals.DISCORD_BOT.bot_thread.is_alive() or globals.REDDIT_BOT.bot_thread.is_alive():

Check warning on line 25 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L25

Added line #L25 was not covered by tests
time.sleep(0.5)
except KeyboardInterrupt:
print("Keyboard Interrupt Detected")
discord_bot.stop()
reddit_bot.stop()
globals.DISCORD_BOT.stop()
globals.REDDIT_BOT.stop()

Check warning on line 30 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L29-L30

Added lines #L29 - L30 were not covered by tests


if __name__ == '__main__': # pragma: no cover
Expand Down
Empty file added src/common/__init__.py
Empty file.
21 changes: 17 additions & 4 deletions src/common.py → src/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
import requests


colors = {
'black': 0x000000,
'green': 0x00ff00,
'orange': 0xffa500,
'purple': 0x9147ff,
'red': 0xff0000,
'white': 0xffffff,
'yellow': 0xffff00,
}


def get_bot_avatar(gravatar: str) -> str:
"""
Get Gravatar image url.
Expand Down Expand Up @@ -36,21 +47,23 @@ def get_avatar_bytes():
return avatar_img


def get_data_dir():
def get_app_dirs():
# parent directory name of this file, not full path
parent_dir = os.path.dirname(os.path.abspath(__file__)).split(os.sep)[-2]
parent_dir = os.path.dirname(os.path.abspath(__file__)).split(os.sep)[-3]
if parent_dir == 'app': # running in Docker container
a = '/app'

Check warning on line 54 in src/common/common.py

View check run for this annotation

Codecov / codecov/patch

src/common/common.py#L54

Added line #L54 was not covered by tests
d = '/data'
else: # running locally
a = os.getcwd()
d = os.path.join(os.getcwd(), 'data')
os.makedirs(d, exist_ok=True)
return d
return a, d


# constants
avatar = get_bot_avatar(gravatar=os.environ['GRAVATAR_EMAIL'])
org_name = 'LizardByte'
bot_name = f'{org_name}-Bot'
bot_url = 'https://app.lizardbyte.dev'
data_dir = get_data_dir()
app_dir, data_dir = get_app_dirs()
version = '0.0.0'
69 changes: 69 additions & 0 deletions src/common/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# standard imports
import os

# lib imports
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
from datetime import datetime, timedelta, UTC

# local imports
from src.common import common

CERT_FILE = os.path.join(common.data_dir, "cert.pem")
KEY_FILE = os.path.join(common.data_dir, "key.pem")


def check_expiration(cert_path: str) -> int:
with open(cert_path, "rb") as cert_file:
cert_data = cert_file.read()
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
expiry_date = cert.not_valid_after_utc
return (expiry_date - datetime.now(UTC)).days


def generate_certificate():
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
subject = issuer = x509.Name([
x509.NameAttribute(x509.NameOID.COMMON_NAME, u"localhost"),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.now(UTC)
).not_valid_after(
datetime.now(UTC) + timedelta(days=365)
).sign(private_key, hashes.SHA256())

with open(KEY_FILE, "wb") as f:
f.write(private_key.private_bytes(
encoding=Encoding.PEM,
format=PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=NoEncryption(),
))

with open(CERT_FILE, "wb") as f:
f.write(cert.public_bytes(Encoding.PEM))


def initialize_certificate() -> tuple[str, str]:
print("Initializing SSL certificate")
if os.path.exists(CERT_FILE) and os.path.exists(KEY_FILE):
cert_expires_in = check_expiration(CERT_FILE)
print(f"Certificate expires in {cert_expires_in} days.")
if cert_expires_in >= 90:
return CERT_FILE, KEY_FILE
print("Generating new certificate")
generate_certificate()
return CERT_FILE, KEY_FILE
22 changes: 22 additions & 0 deletions src/common/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# standard imports
import shelve
import threading


class Database:
def __init__(self, db_path):
self.db_path = db_path
self.lock = threading.Lock()

def __enter__(self):
self.lock.acquire()
self.db = shelve.open(self.db_path, writeback=True)
return self.db

def __exit__(self, exc_type, exc_val, exc_tb):
self.sync()
self.db.close()
self.lock.release()

def sync(self):
self.db.sync()
2 changes: 2 additions & 0 deletions src/common/globals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DISCORD_BOT = None
REDDIT_BOT = None
Loading

0 comments on commit 0fd9c62

Please sign in to comment.