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 Nov 23, 2024
1 parent 362aef2 commit 7bdfdcf
Show file tree
Hide file tree
Showing 29 changed files with 1,025 additions and 312 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ jobs:
- name: Test with pytest
id: test
env:
CI_EVENT_ID: ${{ github.event.number || github.sha }}
GITHUB_PYTEST: "true"
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 }}
PRAW_CLIENT_ID: ${{ secrets.REDDIT_CLIENT_ID }}
PRAW_CLIENT_SECRET: ${{ secrets.REDDIT_CLIENT_SECRET }}
Expand Down
21 changes: 14 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,42 @@ 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 GRAVATAR_EMAIL
ARG IGDB_CLIENT_ID
ARG IGDB_CLIENT_SECRET

Check warning on line 32 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

Check warning on line 34 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 "PRAW_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
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 42 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 44 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 GRAVATAR_EMAIL=$GRAVATAR_EMAIL
ENV IGDB_CLIENT_ID=$IGDB_CLIENT_ID
ENV IGDB_CLIENT_SECRET=$IGDB_CLIENT_SECRET

Check warning on line 51 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 "IGDB_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV PRAW_CLIENT_ID=$PRAW_CLIENT_ID
ENV PRAW_CLIENT_SECRET=$PRAW_CLIENT_SECRET

Check warning on line 53 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 "PRAW_CLIENT_SECRET") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV PRAW_SUBREDDIT=$PRAW_SUBREDDIT
ENV DISCORD_WEBHOOK=$DISCORD_WEBHOOK

Check warning on line 55 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 +76,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
51 changes: 29 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,24 @@ 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. |
| 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 +63,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`
1 change: 0 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
betamax==0.9.0
betamax-serializers==0.2.1
pytest==8.3.3
pytest-asyncio==0.24.0
pytest-cov==6.0.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.0.3
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
26 changes: 10 additions & 16 deletions src/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# standard imports
import os
import time

# development imports
Expand All @@ -8,33 +7,28 @@

# local imports
if True: # hack for flake8
from src.common import globals

Check warning on line 10 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L10

Added line #L10 was not covered by tests
from src.discord import bot as d_bot
from src import keep_alive
from src.common import webapp

Check warning on line 12 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L12

Added line #L12 was not covered by tests
from src.reddit import bot as r_bot


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 17 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L17

Added line #L17 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 20 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L19-L20

Added lines #L19 - L20 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 23 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L22-L23

Added lines #L22 - L23 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 26 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L26

Added line #L26 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 31 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L30-L31

Added lines #L30 - L31 were not covered by tests


if __name__ == '__main__': # pragma: no cover
Expand Down
Empty file added src/common/__init__.py
Empty file.
File renamed without changes.
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

Check warning on line 69 in src/common/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/common/crypto.py#L67-L69

Added lines #L67 - L69 were not covered by tests
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
73 changes: 73 additions & 0 deletions src/common/sponsors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# standard imports
import os
from typing import Union

# lib imports
import requests


tier_map = {
't4-sponsors': 15,
't3-sponsors': 10,
't2-sponsors': 5,
't1-sponsors': 3,
}


def get_github_sponsors() -> Union[dict, False]:
"""
Get list of GitHub sponsors.
Returns
-------
Union[dict, False]
JSON response containing the list of sponsors. False if an error occurred.
"""
token = os.getenv("GITHUB_TOKEN")
org_name = os.getenv("GITHUB_ORG_NAME", "LizardByte")

Check warning on line 27 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L26-L27

Added lines #L26 - L27 were not covered by tests

graphql_url = "https://api.github.com/graphql"
headers = {

Check warning on line 30 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L29-L30

Added lines #L29 - L30 were not covered by tests
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
query = """

Check warning on line 34 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L34

Added line #L34 was not covered by tests
query {
organization(login: "%s") {
sponsorshipsAsMaintainer(first: 100) {
edges {
node {
sponsorEntity {
... on User {
login
name
avatarUrl
url
}
... on Organization {
login
name
avatarUrl
url
}
}
tier {
name
monthlyPriceInDollars
}
}
}
}
}
}
""" % org_name

response = requests.post(graphql_url, json={'query': query}, headers=headers)
data = response.json()

Check warning on line 66 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L65-L66

Added lines #L65 - L66 were not covered by tests

if 'errors' in data:
print(data['errors'])
print('::error::An error occurred while fetching sponsors.')
return False

Check warning on line 71 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L68-L71

Added lines #L68 - L71 were not covered by tests

return data

Check warning on line 73 in src/common/sponsors.py

View check run for this annotation

Codecov / codecov/patch

src/common/sponsors.py#L73

Added line #L73 was not covered by tests
Loading

0 comments on commit 7bdfdcf

Please sign in to comment.