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 9, 2024
1 parent 362aef2 commit 14cb7f9
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 29 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ praw==7.8.1
py-cord==2.6.1
python-dotenv==1.0.1
requests==2.32.3
requests-oauthlib==2.0.0
11 changes: 2 additions & 9 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 @@ -9,18 +8,12 @@
# local imports
if True: # hack for flake8
from src.discord import bot as d_bot
from src import keep_alive
from src import webapp

Check warning on line 11 in src/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/__main__.py#L11

Added line #L11 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 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
Expand Down
69 changes: 69 additions & 0 deletions src/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# standard imports
import os

Check warning on line 2 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L2

Added line #L2 was not covered by tests

# 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

Check warning on line 10 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L5-L10

Added lines #L5 - L10 were not covered by tests

# local imports
from src import common

Check warning on line 13 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L13

Added line #L13 was not covered by tests

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

Check warning on line 16 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L15-L16

Added lines #L15 - L16 were not covered by tests


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

Check warning on line 24 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L19-L24

Added lines #L19 - L24 were not covered by tests


def generate_certificate():
private_key = rsa.generate_private_key(

Check warning on line 28 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L27-L28

Added lines #L27 - L28 were not covered by tests
public_exponent=65537,
key_size=4096,
)
subject = issuer = x509.Name([

Check warning on line 32 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L32

Added line #L32 was not covered by tests
x509.NameAttribute(x509.NameOID.COMMON_NAME, u"localhost"),
])
cert = x509.CertificateBuilder().subject_name(

Check warning on line 35 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L35

Added line #L35 was not covered by tests
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(

Check warning on line 50 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L49-L50

Added lines #L49 - L50 were not covered by tests
encoding=Encoding.PEM,
format=PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=NoEncryption(),
))

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

Check warning on line 57 in src/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L56-L57

Added lines #L56 - L57 were not covered by tests


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/crypto.py

View check run for this annotation

Codecov / codecov/patch

src/crypto.py#L60-L69

Added lines #L60 - L69 were not covered by tests
118 changes: 118 additions & 0 deletions src/discord/cogs/github_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# standard imports
import os

# lib imports
import discord
import requests
from requests_oauthlib import OAuth2Session


class GitHubCommandsCog(discord.Cog):
def __init__(self, bot):
self.bot = bot
self.token = os.getenv("GITHUB_TOKEN")
self.org_name = os.getenv("GITHUB_ORG_NAME", "LizardByte")
self.graphql_url = "https://api.github.com/graphql"
self.headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}

@discord.slash_command(
name="get_sponsors",
description="Get list of GitHub sponsors",
default_member_permissions=discord.Permissions(manage_guild=True),
)
async def get_sponsors(
self,
ctx: discord.ApplicationContext,
):
"""
Get list of GitHub sponsors.
Parameters
----------
ctx : discord.ApplicationContext
Request message context.
"""
query = """

Check warning on line 38 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L38

Added line #L38 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
}
}
}
}
}
}
""" % self.org_name

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

Check warning on line 70 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L69-L70

Added lines #L69 - L70 were not covered by tests

if 'errors' in data:
print(data['errors'])
await ctx.respond("An error occurred while fetching sponsors.", ephemeral=True)
return

Check warning on line 75 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L72-L75

Added lines #L72 - L75 were not covered by tests

message = "List of GitHub sponsors"
for edge in data['data']['organization']['sponsorshipsAsMaintainer']['edges']:
sponsor = edge['node']['sponsorEntity']
tier = edge['node'].get('tier', {})
tier_info = f" - Tier: {tier.get('name', 'N/A')} (${tier.get('monthlyPriceInDollars', 'N/A')}/month)"
message += f"\n* [{sponsor['login']}]({sponsor['url']}){tier_info}"

Check warning on line 82 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L77-L82

Added lines #L77 - L82 were not covered by tests

embed = discord.Embed(title="GitHub Sponsors", color=0x00ff00, description=message)

Check warning on line 84 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L84

Added line #L84 was not covered by tests

await ctx.respond(embed=embed, ephemeral=True)

Check warning on line 86 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L86

Added line #L86 was not covered by tests

@discord.slash_command(
name="link_github",
description="Validate GitHub sponsor status"
)
async def link_github(self, ctx: discord.ApplicationContext):
"""
Link Discord account with GitHub account, by validating Discord user's "GitHub" connected account status.
User to login to Discord via OAuth2, and check if their connected GitHub account is a sponsor of the project.
Parameters
----------
ctx : discord.ApplicationContext
Request message context.
"""
discord_oauth = OAuth2Session(

Check warning on line 103 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L103

Added line #L103 was not covered by tests
os.environ['DISCORD_CLIENT_ID'],
redirect_uri=os.environ['DISCORD_REDIRECT_URI'],
scope=[
"identify",
"connections",
],
)
authorization_url, state = discord_oauth.authorization_url("https://discord.com/oauth2/authorize")

Check warning on line 111 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L111

Added line #L111 was not covered by tests

# Store the state in the user's session or database
await ctx.respond(f"Please authorize the application by clicking [here]({authorization_url}).", ephemeral=True)

Check warning on line 114 in src/discord/cogs/github_commands.py

View check run for this annotation

Codecov / codecov/patch

src/discord/cogs/github_commands.py#L114

Added line #L114 was not covered by tests


def setup(bot: discord.Bot):
bot.add_cog(GitHubCommandsCog(bot=bot))
20 changes: 0 additions & 20 deletions src/keep_alive.py

This file was deleted.

65 changes: 65 additions & 0 deletions src/webapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# standard imports
import os
from threading import Thread

Check warning on line 3 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L2-L3

Added lines #L2 - L3 were not covered by tests

# lib imports
from flask import Flask, request
from requests_oauthlib import OAuth2Session

Check warning on line 7 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L6-L7

Added lines #L6 - L7 were not covered by tests

# local imports
from src import crypto

Check warning on line 10 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L10

Added line #L10 was not covered by tests


DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID")
DISCORD_CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET")
DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI")

Check warning on line 15 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L13-L15

Added lines #L13 - L15 were not covered by tests

app = Flask('LizardByte-bot')

Check warning on line 17 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L17

Added line #L17 was not covered by tests


@app.route('/')
def main():
return "LizardByte-bot is live!"

Check warning on line 22 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L20-L22

Added lines #L20 - L22 were not covered by tests


@app.route("/discord/callback")
def discord_callback():
discord_oauth = OAuth2Session(DISCORD_CLIENT_ID, redirect_uri=DISCORD_REDIRECT_URI)
token = discord_oauth.fetch_token("https://discord.com/api/oauth2/token",

Check warning on line 28 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L25-L28

Added lines #L25 - L28 were not covered by tests
client_secret=DISCORD_CLIENT_SECRET,
authorization_response=request.url)
print(token)

Check warning on line 31 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L31

Added line #L31 was not covered by tests

# Fetch the user's Discord profile
response = discord_oauth.get("https://discord.com/api/users/@me")
discord_user = response.json()
print(discord_user)

Check warning on line 36 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L34-L36

Added lines #L34 - L36 were not covered by tests

# Fetch the user's connected accounts
connections_response = discord_oauth.get("https://discord.com/api/users/@me/connections")
connections = connections_response.json()
print(connections)

Check warning on line 41 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L39-L41

Added lines #L39 - L41 were not covered by tests

# Here you can link the GitHub account with the Discord user
# For example, store the GitHub user ID and Discord user ID in your database

return "Discord account linked successfully!"

Check warning on line 46 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L46

Added line #L46 was not covered by tests


def run():
cert_file, key_file = crypto.initialize_certificate()

Check warning on line 50 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L49-L50

Added lines #L49 - L50 were not covered by tests

app.run(

Check warning on line 52 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L52

Added line #L52 was not covered by tests
host="0.0.0.0",
port=8080,
ssl_context=(cert_file, key_file)
)


def start():
server = Thread(

Check warning on line 60 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L59-L60

Added lines #L59 - L60 were not covered by tests
name="Flask",
daemon=True,
target=run,
)
server.start()

Check warning on line 65 in src/webapp.py

View check run for this annotation

Codecov / codecov/patch

src/webapp.py#L65

Added line #L65 was not covered by tests

0 comments on commit 14cb7f9

Please sign in to comment.