mirror of
https://github.com/MichaByte/OnBoard-Live.git
synced 2025-12-06 08:33:40 -05:00
major refactoring + docker progress
This commit is contained in:
parent
ecb8a80f8b
commit
e75f8c944a
35 changed files with 227 additions and 104 deletions
21
backend/Dockerfile
Normal file
21
backend/Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y python3-opencv
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY main.py schema.prisma .
|
||||
|
||||
COPY migrations .
|
||||
|
||||
RUN prisma generate
|
||||
|
||||
CMD [ "fastapi", "run", "main.py" ]
|
||||
836
backend/main.py
Normal file
836
backend/main.py
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from secrets import choice, token_hex
|
||||
from typing import Dict, List
|
||||
|
||||
import cv2
|
||||
import httpx
|
||||
import uvicorn
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from cryptography.fernet import Fernet
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from prisma import Prisma
|
||||
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
|
||||
from slack_bolt.async_app import AsyncAck, AsyncApp
|
||||
from yarl import URL
|
||||
|
||||
load_dotenv(dotenv_path="./.env")
|
||||
|
||||
active_stream: Dict[str, str | bool] = {}
|
||||
active_streams: List[Dict[str, str | bool]] = []
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
FERNET_KEY = Fernet.generate_key()
|
||||
FERNET_KEY_USERS = []
|
||||
|
||||
|
||||
FERNET = Fernet(FERNET_KEY)
|
||||
|
||||
|
||||
async def rotate_fernet_key():
|
||||
global FERNET_KEY
|
||||
global FERNET
|
||||
if FERNET_KEY_USERS == []:
|
||||
FERNET_KEY = Fernet.generate_key()
|
||||
FERNET = Fernet(FERNET_KEY)
|
||||
else:
|
||||
print("not rotating key since we have a pending verification")
|
||||
|
||||
|
||||
def get_recording_duration(timestamp, stream_key):
|
||||
vid = cv2.VideoCapture(
|
||||
f"/home/onboard/recordings/{stream_key}/{datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ').strftime('%Y-%m-%d_%H-%M-%S-%f')}.mp4"
|
||||
)
|
||||
return int(
|
||||
(vid.get(cv2.CAP_PROP_FRAME_COUNT) / vid.get(cv2.CAP_PROP_FPS)) / 60
|
||||
) # seconds to minutes
|
||||
|
||||
|
||||
def verify_gh_signature(payload_body, secret_token, signature_header):
|
||||
"""Verify that the payload was sent from GitHub by validating SHA256.
|
||||
|
||||
Raise and return 403 if not authorized.
|
||||
|
||||
Args:
|
||||
payload_body: original request body to verify (request.body())
|
||||
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
|
||||
signature_header: header received from GitHub (x-hub-signature-256)
|
||||
"""
|
||||
if not signature_header:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="x-hub-signature-256 header is missing!"
|
||||
)
|
||||
hash_object = hmac.new(
|
||||
secret_token.encode("utf-8"), msg=payload_body, digestmod=hashlib.sha256
|
||||
)
|
||||
expected_signature = "sha256=" + hash_object.hexdigest()
|
||||
if not hmac.compare_digest(expected_signature, signature_header):
|
||||
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
|
||||
|
||||
|
||||
async def get_recording_list(stream_key: str) -> List[str]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
return [
|
||||
recording["start"]
|
||||
for recording in (
|
||||
await client.get(
|
||||
f"http://{os.environ['MEDIAMTX_IP']}:9997/v3/recordings/get/{stream_key}"
|
||||
)
|
||||
).json()["segments"]
|
||||
]
|
||||
|
||||
|
||||
async def update_active():
|
||||
global active_stream
|
||||
global active_streams
|
||||
async with httpx.AsyncClient() as client:
|
||||
streams_raw = (await client.get(f"http://{os.environ['MEDIAMTX_IP']}:9997/v3/paths/list")).json()[
|
||||
"items"
|
||||
]
|
||||
streams = []
|
||||
for stream in streams_raw:
|
||||
streams.append({"name": stream["name"], "ready": stream["ready"]})
|
||||
for stream in streams:
|
||||
if stream["ready"] and stream not in active_streams:
|
||||
active_streams.append(stream)
|
||||
if len(active_streams) == 0:
|
||||
return
|
||||
if active_stream == {}:
|
||||
active_stream = choice(active_streams)
|
||||
return
|
||||
if len(active_streams) == 1:
|
||||
return
|
||||
new_stream = choice(active_streams)
|
||||
while new_stream["name"] == active_stream["name"]:
|
||||
new_stream = choice(active_streams)
|
||||
old_active_stream_user = await db.user.find_first(where={"id": (await db.stream.find_first(where={"key": str(active_stream["name"])})).user_id}) # type: ignore
|
||||
await bolt.client.chat_postMessage(channel="C07ERCGG989", text=f"Hey <@{old_active_stream_user.slack_id}>, you're no longer in focus!") # type: ignore
|
||||
active_stream = new_stream
|
||||
active_stream_user = await db.user.find_first(where={"id": (await db.stream.find_first(where={"key": str(active_stream["name"])})).user_id}) # type: ignore
|
||||
await bolt.client.chat_postMessage(channel="C07ERCGG989", text=f"Hey <@{active_stream_user.slack_id}>, you're in focus! Make sure to tell us what you're working on!") # type: ignore
|
||||
return True
|
||||
|
||||
|
||||
async def check_for_new():
|
||||
global active_stream
|
||||
global active_streams
|
||||
async with httpx.AsyncClient() as client:
|
||||
streams_raw = (await client.get(f"http://{os.environ['MEDIAMTX_IP']}:9997/v3/paths/list")).json()[
|
||||
"items"
|
||||
]
|
||||
streams_simple = []
|
||||
for stream in streams_raw:
|
||||
if stream["ready"]:
|
||||
streams_simple.append(stream["name"])
|
||||
active_streams_simple = []
|
||||
for i in active_streams:
|
||||
active_streams_simple.append(i["name"])
|
||||
if active_stream == {}:
|
||||
active_stream = {"name": i["name"], "ready": True}
|
||||
for stream in active_streams_simple:
|
||||
if stream not in streams_simple:
|
||||
active_streams.remove(
|
||||
next(item for item in active_streams if item["name"] == stream)
|
||||
)
|
||||
active_stream = choice(active_streams)
|
||||
for stream in streams_simple:
|
||||
if stream not in active_streams_simple:
|
||||
active_streams.append({"name": stream, "ready": True})
|
||||
if len(active_streams) == 0:
|
||||
active_stream = {}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await update_active()
|
||||
scheduler.start()
|
||||
scheduler.add_job(update_active, IntervalTrigger(minutes=5))
|
||||
scheduler.add_job(check_for_new, IntervalTrigger(seconds=3))
|
||||
scheduler.add_job(rotate_fernet_key, IntervalTrigger(minutes=30))
|
||||
await rotate_fernet_key()
|
||||
await db.connect()
|
||||
async with httpx.AsyncClient() as client:
|
||||
for stream in await db.stream.find_many():
|
||||
await client.post(
|
||||
f"http://{os.environ['MEDIAMTX_IP']}:9997/v3/config/paths/add/" + stream.key,
|
||||
json={"name": stream.key},
|
||||
)
|
||||
yield
|
||||
scheduler.shutdown()
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
api = FastAPI(lifespan=lifespan) # type: ignore
|
||||
|
||||
api.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
db = Prisma()
|
||||
|
||||
bolt = AsyncApp(
|
||||
token=os.environ["SLACK_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"]
|
||||
)
|
||||
|
||||
bolt_handler = AsyncSlackRequestHandler(bolt)
|
||||
|
||||
|
||||
@api.get("/auth/github/login")
|
||||
async def github_redirect(request: Request):
|
||||
return RedirectResponse(
|
||||
str(
|
||||
URL.build(
|
||||
scheme="https",
|
||||
host="github.com",
|
||||
path="/login/oauth/authorize",
|
||||
query={
|
||||
"client_id": os.environ["GH_CLIENT_ID"],
|
||||
"redirect_uri": "https://live.onboard.hackclub.com/auth/github/callback",
|
||||
"scopes": "read:user",
|
||||
"state": request.query_params["state"],
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@api.get("/auth/github/callback")
|
||||
async def github_callback(request: Request):
|
||||
code: str = request.query_params["code"]
|
||||
state: str = request.query_params["state"]
|
||||
user_id, pr_id = FERNET.decrypt(bytes.fromhex(state)).decode().split("+")
|
||||
if user_id in FERNET_KEY_USERS:
|
||||
FERNET_KEY_USERS.remove(user_id)
|
||||
db_user = await db.user.find_first_or_raise(where={"slack_id": user_id})
|
||||
user_stream_key = (
|
||||
await db.stream.find_first_or_raise(where={"user_id": db_user.id})
|
||||
).key
|
||||
db_pr = await db.pullrequest.find_first_or_raise(where={"github_id": int(pr_id)})
|
||||
async with httpx.AsyncClient() as client:
|
||||
token = (
|
||||
await client.post(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
json={
|
||||
"client_id": os.environ["GH_CLIENT_ID"],
|
||||
"client_secret": os.environ["GH_CLIENT_SECRET"],
|
||||
"code": code,
|
||||
"redirect_uri": "https://live.onboard.hackclub.com/auth/github/callback",
|
||||
},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
).json()["access_token"]
|
||||
|
||||
gh_user: int = (
|
||||
await client.get(
|
||||
"https://api.github.com/user",
|
||||
headers={
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
},
|
||||
)
|
||||
).json()["id"]
|
||||
if gh_user == db_pr.gh_user_id:
|
||||
await db.pullrequest.update(
|
||||
{"user": {"connect": {"id": db_user.id}}, "gh_user_id": gh_user},
|
||||
{"id": db_pr.id},
|
||||
)
|
||||
stream_recs = await get_recording_list(user_stream_key)
|
||||
if stream_recs == []:
|
||||
return HTMLResponse(
|
||||
"<h1>You don't have any sessions to submit! Please DM @mra on Slack if you think this is a mistake.</h1>"
|
||||
)
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=user_id,
|
||||
text="Select your OnBoard Live sessions!",
|
||||
blocks=[
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Select your sessions for review!\nCopy and paste the lines of sessions that you want associated with this PR into the box!",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"block_id": "session-checks",
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"Here are all your sessions. Select the ones associated with OnBoard pull request #{pr_id}:",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"{'\n'.join([recording + ' for ' + str(get_recording_duration(recording, user_stream_key)) + 'minutes' for recording in stream_recs])}", # type: ignore
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "session-input",
|
||||
"element": {
|
||||
"type": "plain_text_input",
|
||||
"multiline": True,
|
||||
"action_id": "plain_text_input-action",
|
||||
},
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "Paste the lines here (DO NOT EDIT THEM, ONE ON EACH LINE)",
|
||||
"emoji": False,
|
||||
},
|
||||
},
|
||||
# "block_id": "session-checks",
|
||||
# "type": "section",
|
||||
# "text": {
|
||||
# "type": "mrkdwn",
|
||||
# "text": f"Here are all your sessions. Select the ones associated with OnBoard pull request #{pr_id}:",
|
||||
# },
|
||||
# "accessory": {
|
||||
# "type": "checkboxes",
|
||||
# "options": [
|
||||
# json.loads(
|
||||
# """{{"text": {{ "type": "mrkdwn", "text": "Your session on {pretty_time}"}}, "description": {{"type": "mrkdwn", "text": "You streamed for {length} {minute_or_minutes}"}}, "value": "checkbox-{filename}"}}""".format(
|
||||
# pretty_time=recording,
|
||||
# length=get_recording_duration(
|
||||
# recording, user_stream_key
|
||||
# ),
|
||||
# minute_or_minutes=(
|
||||
# "minute"
|
||||
# if get_recording_duration(
|
||||
# recording, user_stream_key
|
||||
# )
|
||||
# == 1
|
||||
# else "minutes"
|
||||
# ),
|
||||
# filename=recording,
|
||||
# )
|
||||
# )
|
||||
# for recording in stream_recs
|
||||
# ],
|
||||
# "action_id": "checkboxes",
|
||||
# },
|
||||
# },
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"emoji": True,
|
||||
"text": "Submit",
|
||||
},
|
||||
"style": "primary",
|
||||
"value": "submit_sessions",
|
||||
"action_id": "submit_sessions",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return HTMLResponse(
|
||||
"<h1>Success! Your PR has been linked to your Slack account. Check your Slack DMs for the next steps!</h1>"
|
||||
)
|
||||
return HTMLResponse(
|
||||
"<h1>Looks like something went wrong! DM @mra on slack.</h1>",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@api.post("/api/v1/github/pr_event")
|
||||
async def pr_event(request: Request):
|
||||
verify_gh_signature(
|
||||
await request.body(),
|
||||
os.environ["GH_HOOK_SECRET"],
|
||||
request.headers.get("x-hub-signature-256"),
|
||||
)
|
||||
body = json.loads(await request.body())
|
||||
if body["action"] == "labeled":
|
||||
if body["label"]["id"] == 7336079497:
|
||||
await db.pullrequest.create(
|
||||
{
|
||||
"github_id": body["pull_request"]["number"],
|
||||
"gh_user_id": body["pull_request"]["user"]["id"],
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@api.get("/api/v1/stream_key/{stream_key}")
|
||||
async def get_stream_by_key(stream_key: str):
|
||||
stream = await db.stream.find_first(where={"key": stream_key})
|
||||
return (
|
||||
stream if stream else Response(status_code=404, content="404: Stream not found")
|
||||
)
|
||||
|
||||
|
||||
@api.get("/api/v1/active_stream")
|
||||
async def get_active_stream():
|
||||
return active_stream["name"] if "name" in active_stream else ""
|
||||
|
||||
|
||||
@bolt.event("app_home_opened")
|
||||
async def handle_app_home_opened_events(body, logger, event, client):
|
||||
await client.views_publish(
|
||||
user_id=event["user"],
|
||||
# the view object that appears in the app home
|
||||
view={
|
||||
"type": "home",
|
||||
"callback_id": "home_view",
|
||||
# body of the view
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Welcome to OnBoard Live! Try sending `/onboard-live-apply` in the #onboard-live channel to get started!",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@bolt.action("submit_sessions")
|
||||
async def submit_sessions(ack: AsyncAck, body):
|
||||
await ack()
|
||||
selected_sessions_ts: List[str] = []
|
||||
print(body["state"]["values"])
|
||||
for session in body["state"]["values"]["session-input"]["plain_text_input-action"][
|
||||
"value"
|
||||
].split("\n"):
|
||||
selected_sessions_ts.append(session.split(" for ")[0])
|
||||
|
||||
pr_id = int(
|
||||
body["message"]["blocks"][1]["text"]["text"].split("#")[1].split(":")[0]
|
||||
) # don't tell my mom she raised a monster
|
||||
db_pr = await db.pullrequest.find_first_or_raise(where={"github_id": pr_id})
|
||||
if db_pr.user_id:
|
||||
stream_key = (
|
||||
await db.stream.find_first_or_raise(where={"user_id": db_pr.user_id})
|
||||
).key
|
||||
for session in selected_sessions_ts:
|
||||
await db.session.create(
|
||||
{
|
||||
"pull": {"connect": {"id": db_pr.id}},
|
||||
"timestamp": session,
|
||||
"filename": f"/home/onboard/recordings/{stream_key}/{datetime.strptime(session, '%Y-%m-%dT%H:%M:%S.%fZ').strftime('%Y-%m-%d_%H-%M-%S-%f')}.mp4",
|
||||
"duration": get_recording_duration(session, stream_key),
|
||||
}
|
||||
)
|
||||
await bolt.client.chat_delete(
|
||||
channel=body["container"]["channel_id"], ts=body["message"]["ts"]
|
||||
)
|
||||
print(pr_id, selected_sessions_ts)
|
||||
|
||||
|
||||
@bolt.action("deny")
|
||||
async def deny(ack, body):
|
||||
await ack()
|
||||
message = body["message"]
|
||||
applicant_slack_id = message["blocks"][len(message) - 3]["text"]["text"].split(
|
||||
": "
|
||||
)[
|
||||
1
|
||||
] # I hate it. You hate it. We all hate it. Carry on.
|
||||
applicant_name = message["blocks"][len(message) - 7]["text"]["text"].split(
|
||||
"Name: "
|
||||
)[
|
||||
1
|
||||
] # oops i did it again
|
||||
await bolt.client.chat_delete(
|
||||
channel=body["container"]["channel_id"], ts=message["ts"]
|
||||
)
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=body["container"]["channel_id"],
|
||||
text=f"{applicant_name}'s application has been denied! Remember to reach out to them if this is a fixable issue. Their username is <@{applicant_slack_id}>.",
|
||||
)
|
||||
|
||||
|
||||
@bolt.action("approve")
|
||||
async def approve(ack, body):
|
||||
await ack()
|
||||
message = body["message"]
|
||||
applicant_slack_id = message["blocks"][len(message) - 3]["text"]["text"].split(
|
||||
": "
|
||||
)[
|
||||
1
|
||||
] # I hate it. You hate it. We all hate it. Carry on.
|
||||
applicant_name = message["blocks"][len(message) - 7]["text"]["text"].split(
|
||||
"Name: "
|
||||
)[
|
||||
1
|
||||
] # oops i did it again
|
||||
await bolt.client.chat_delete(
|
||||
channel=body["container"]["channel_id"], ts=message["ts"]
|
||||
)
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=body["container"]["channel_id"],
|
||||
text=f"{applicant_name}'s application has been approved! Their username is <@{applicant_slack_id}>.",
|
||||
)
|
||||
if applicant_slack_id in [d.slack_id for d in await db.user.find_many()]: # type: ignore
|
||||
return
|
||||
new_user = await db.user.create(
|
||||
{"slack_id": applicant_slack_id, "name": applicant_name}
|
||||
)
|
||||
new_stream = await db.stream.create(
|
||||
{"user": {"connect": {"id": new_user.id}}, "key": token_hex(16)}
|
||||
)
|
||||
sumbitter_convo = await bolt.client.conversations_open(
|
||||
users=applicant_slack_id, return_im=True
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(
|
||||
f"http://{os.environ['MEDIAMTX_IP']}:9997/v3/config/paths/add/" + new_stream.key,
|
||||
json={"name": new_stream.key},
|
||||
)
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=sumbitter_convo["channel"]["id"],
|
||||
text=f"Welcome to OnBoard Live! Your stream key is {new_stream.key}. To use your stream key the easy way, go to <https://live.onboard.hackclub.com/{new_stream.key}/publish|this link>. You can also use it in OBS with the server URL of rtmp://live.onboard.hackclub.com:1935",
|
||||
)
|
||||
|
||||
|
||||
@bolt.view("apply")
|
||||
async def handle_application_submission(ack, body):
|
||||
await ack()
|
||||
user = body["user"]["id"]
|
||||
sumbitter_convo = await bolt.client.conversations_open(users=user, return_im=True)
|
||||
user_real_name = (await bolt.client.users_info(user=user))["user"]["real_name"]
|
||||
user_verified = ""
|
||||
async with httpx.AsyncClient() as client:
|
||||
user_verified = (
|
||||
"Eligible L"
|
||||
not in (
|
||||
await client.request(
|
||||
url="https://verify.hackclub.dev/api/status",
|
||||
method="GET",
|
||||
content=json.dumps({"slack_id": user}),
|
||||
)
|
||||
).text
|
||||
)
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=sumbitter_convo["channel"]["id"],
|
||||
text=f"Your application has been submitted! We will review it shortly. Please do not send another application - If you haven't heard back in over 48 hours, or you forgot something in your application, please message <@{os.environ['ADMIN_SLACK_ID']}>! Here's a copy of your responses for your reference:\nSome info on your project(s): {body['view']['state']['values']['project-info']['project-info-body']['value']}\n{f'Please fill out <https://forms.hackclub.com/eligibility?program=Onboard%20Live&slack_id={user}|the verification form>! We can only approve your application once this is done.' if not user_verified else ''}",
|
||||
)
|
||||
will_behave = True
|
||||
# boxes = body["view"]["state"]["values"]["kAgeY"]["checkboxes"]["selected_options"]
|
||||
# if len(boxes) == 1 and boxes[0]["value"] == "value-1":
|
||||
# will_behave = True
|
||||
await bolt.client.chat_postMessage(
|
||||
channel=os.environ["ADMIN_SLACK_ID"],
|
||||
text="New OnBoard Live application!",
|
||||
blocks=[
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":siren-real: New OnBoard Live application! :siren-real:",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":technologist: Name: {user_real_name}",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":white_check_mark: Is verified: {user_verified}",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":hammer_and_wrench: Will make: {body['view']['state']['values']['project-info']['project-info-body']['value']}",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":pray: Will behave on stream: {will_behave}",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f"Slack ID: {user}",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"emoji": True,
|
||||
"text": "Approve",
|
||||
},
|
||||
"style": "primary",
|
||||
"value": "approve",
|
||||
"action_id": "approve",
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"emoji": True,
|
||||
"text": "Deny",
|
||||
},
|
||||
"style": "danger",
|
||||
"value": "deny",
|
||||
"action_id": "deny",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@bolt.command("/onboard-live-submit")
|
||||
async def submit(ack: AsyncAck, command):
|
||||
await ack()
|
||||
user_id = command["user_id"]
|
||||
channel_id = command["channel_id"]
|
||||
text = command["text"]
|
||||
db_pr = await db.pullrequest.find_first(where={"github_id": int(text)})
|
||||
if db_pr is None:
|
||||
await bolt.client.chat_postEphemeral(
|
||||
channel=channel_id,
|
||||
user=user_id,
|
||||
text="There doesn't seem to be a PR open with that ID! If this seems like a mistake, please message <@U05C64XMMHV> about it!",
|
||||
)
|
||||
return
|
||||
if user_id not in FERNET_KEY_USERS:
|
||||
FERNET_KEY_USERS.append(user_id)
|
||||
await bolt.client.chat_postEphemeral(
|
||||
channel=channel_id,
|
||||
user=user_id,
|
||||
text=f"Please <https://live.onboard.hackclub.com/auth/github/login?state={FERNET.encrypt(bytes(f'{user_id}+{db_pr.github_id}', 'utf-8')).hex()}|click here> to authenticate with GitHub. This helps us verify that this is your PR!",
|
||||
)
|
||||
|
||||
|
||||
@bolt.command("/onboard-live-apply")
|
||||
async def apply(ack: AsyncAck, command):
|
||||
await ack()
|
||||
async with httpx.AsyncClient() as client:
|
||||
(
|
||||
await client.post(
|
||||
"https://slack.com/api/views.open",
|
||||
headers={
|
||||
"Authorization": f"Bearer {os.environ['SLACK_TOKEN']}",
|
||||
"Content-type": "application/json; charset=utf-8",
|
||||
},
|
||||
json={
|
||||
"trigger_id": command["trigger_id"],
|
||||
"unfurl_media": False,
|
||||
"view": {
|
||||
"type": "modal",
|
||||
"callback_id": "apply",
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": "OnBoard Live Application",
|
||||
"emoji": True,
|
||||
},
|
||||
"submit": {
|
||||
"type": "plain_text",
|
||||
"text": "Submit",
|
||||
"emoji": True,
|
||||
},
|
||||
"close": {
|
||||
"type": "plain_text",
|
||||
"text": "Cancel",
|
||||
"emoji": True,
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Welcome to OnBoard Live!\n\n*Please make sure to read this form thoroughly.*\n\nWe can't wait to see what you make!\n\n_Depending on your screen, you might need to scroll down to see the whole form._",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "project-info",
|
||||
"element": {
|
||||
"action_id": "project-info-body",
|
||||
"type": "plain_text_input",
|
||||
"multiline": True,
|
||||
"placeholder": {
|
||||
"type": "plain_text",
|
||||
"text": "I want to make...",
|
||||
},
|
||||
},
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "What do you plan on making?\n\nNote that you can make whatever you want, this is just so we know what level you're at!",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "As a participant in OnBoard Live, you must make sure that all your behavior on stream represents our values.",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Examples of unacceptable behavior include (but are not limited to):\n",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "rich_text_list",
|
||||
"style": "bullet",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Streaming inappropriate content or content that is unrelated to PCB design",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Sharing your stream key with others",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Trying to abuse the system",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Streaming pre-recorded work or work that is not yours",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Inappropriate behavior may result in removal from the Hack Club Slack or other consequences, as stated in the <https://hackclub.com/conduct/|Code of Conduct>. Any use of your stream key is your responsibilty, so don't share it with anyone for any reason. Admins will never ask for your stream key.\n\nPlease report any urgent rule violations by messaging <@U05C64XMMHV>. If they do not respond in 5 minutes, please ping <!subteam^S01E4DN8S0Y|fire-fighters>.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Confirm that you have read the above by following these instructions:",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "checkboxes",
|
||||
"options": [
|
||||
{
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "To agree that you will be well-behaved while you're live, DO NOT check this box. Instead, check the one below.",
|
||||
"emoji": True,
|
||||
},
|
||||
"description": {
|
||||
"type": "mrkdwn",
|
||||
"text": "This is to make sure you're paying attention!",
|
||||
},
|
||||
"value": "value-0",
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "To agree that you will be well-behaved while you're live, check this box.",
|
||||
"emoji": True,
|
||||
},
|
||||
"value": "value-1",
|
||||
},
|
||||
],
|
||||
"action_id": "checkboxes",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "Please ask <@U05C64XMMHV> for help if you need it!",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
).text
|
||||
|
||||
|
||||
@bolt.action("checkboxes")
|
||||
async def checkboxes(ack):
|
||||
"""
|
||||
AFAICT there needs to be *an* action for the checkboxes, but I process their data elsewhere (on submit)
|
||||
To avoid warnings in Slack, I'm just ACKing it here and doing nothing :)
|
||||
"""
|
||||
await ack()
|
||||
|
||||
|
||||
@api.post("/slack/events")
|
||||
async def slack_event_endpoint(req: Request):
|
||||
return await bolt_handler.handle(req)
|
||||
|
||||
|
||||
def main():
|
||||
uvicorn.run(api)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
backend/migrations/20240719191810_init/migration.sql
Normal file
20
backend/migrations/20240719191810_init/migration.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Stream" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"key" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT false,
|
||||
"focused" BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT "Stream_key_fkey" FOREIGN KEY ("key") REFERENCES "User" ("slackId") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"slackId" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Stream_key_key" ON "Stream"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_slackId_key" ON "User"("slackId");
|
||||
41
backend/migrations/20240815175119_pls_work/migration.sql
Normal file
41
backend/migrations/20240815175119_pls_work/migration.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `active` on the `Stream` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `focused` on the `Stream` table. All the data in the column will be lost.
|
||||
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- You are about to drop the column `slackId` on the `User` table. All the data in the column will be lost.
|
||||
- Added the required column `user_id` to the `Stream` table without a default value. This is not possible if the table is not empty.
|
||||
- The required column `id` was added to the `User` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
- Added the required column `slack_id` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Stream" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"is_live" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_focused" BOOLEAN NOT NULL DEFAULT false,
|
||||
"key" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
CONSTRAINT "Stream_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Stream" ("id", "key") SELECT "id", "key" FROM "Stream";
|
||||
DROP TABLE "Stream";
|
||||
ALTER TABLE "new_Stream" RENAME TO "Stream";
|
||||
CREATE UNIQUE INDEX "Stream_key_key" ON "Stream"("key");
|
||||
CREATE UNIQUE INDEX "Stream_user_id_key" ON "Stream"("user_id");
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"slack_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_User" ("name") SELECT "name" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User_slack_id_key" ON "User"("slack_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
10
backend/migrations/20240816163601_add_pr_model/migration.sql
Normal file
10
backend/migrations/20240816163601_add_pr_model/migration.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"userId" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PullRequest_token_key" ON "PullRequest"("token");
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"userId" TEXT,
|
||||
"token" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("id", "token", "userId") SELECT "id", "token", "userId" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_token_key" ON "PullRequest"("token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `github_id` to the `PullRequest` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"userId" TEXT,
|
||||
"token" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("id", "token", "userId") SELECT "id", "token", "userId" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
CREATE UNIQUE INDEX "PullRequest_token_key" ON "PullRequest"("token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- The required column `secondary_token` was added to the `PullRequest` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"userId" TEXT,
|
||||
"token" TEXT NOT NULL,
|
||||
"secondary_token" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("github_id", "id", "token", "userId") SELECT "github_id", "id", "token", "userId" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
CREATE UNIQUE INDEX "PullRequest_token_key" ON "PullRequest"("token");
|
||||
CREATE UNIQUE INDEX "PullRequest_secondary_token_key" ON "PullRequest"("secondary_token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userId` on the `PullRequest` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- CreateTable
|
||||
CREATE TABLE "_PullRequestToPossibleUser" (
|
||||
"A" INTEGER NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_PullRequestToPossibleUser_A_fkey" FOREIGN KEY ("A") REFERENCES "PullRequest" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_PullRequestToPossibleUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"known_user_id" TEXT,
|
||||
"token" TEXT NOT NULL,
|
||||
"secondary_token" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_known_user_id_fkey" FOREIGN KEY ("known_user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("github_id", "id", "secondary_token", "token") SELECT "github_id", "id", "secondary_token", "token" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
CREATE UNIQUE INDEX "PullRequest_token_key" ON "PullRequest"("token");
|
||||
CREATE UNIQUE INDEX "PullRequest_secondary_token_key" ON "PullRequest"("secondary_token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_PullRequestToPossibleUser_AB_unique" ON "_PullRequestToPossibleUser"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_PullRequestToPossibleUser_B_index" ON "_PullRequestToPossibleUser"("B");
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `_PullRequestToPossibleUser` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "_PullRequestToPossibleUser";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `known_user_id` on the `PullRequest` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `secondary_token` on the `PullRequest` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `token` on the `PullRequest` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "github_user_id" TEXT;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"user_id" TEXT,
|
||||
CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("github_id", "id") SELECT "github_id", "id" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "github_token" TEXT;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `github_token` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PullRequest" ADD COLUMN "gh_user_id" TEXT;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"slack_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"github_user_id" TEXT
|
||||
);
|
||||
INSERT INTO "new_User" ("created_at", "github_user_id", "id", "name", "slack_id") SELECT "created_at", "github_user_id", "id", "name", "slack_id" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User_slack_id_key" ON "User"("slack_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `github_user_id` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"slack_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_User" ("created_at", "id", "name", "slack_id") SELECT "created_at", "id", "name", "slack_id" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User_slack_id_key" ON "User"("slack_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `gh_user_id` on table `PullRequest` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"user_id" TEXT,
|
||||
"gh_user_id" TEXT NOT NULL,
|
||||
CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("gh_user_id", "github_id", "id", "user_id") SELECT "gh_user_id", "github_id", "id", "user_id" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `gh_user_id` on the `PullRequest` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_PullRequest" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"github_id" INTEGER NOT NULL,
|
||||
"user_id" TEXT,
|
||||
"gh_user_id" INTEGER NOT NULL,
|
||||
CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_PullRequest" ("gh_user_id", "github_id", "id", "user_id") SELECT "gh_user_id", "github_id", "id", "user_id" FROM "PullRequest";
|
||||
DROP TABLE "PullRequest";
|
||||
ALTER TABLE "new_PullRequest" RENAME TO "PullRequest";
|
||||
CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"duration" INTEGER NOT NULL,
|
||||
"reviewed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"approved" BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT "Session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_user_id_key" ON "Session"("user_id");
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `user_id` on the `Session` table. All the data in the column will be lost.
|
||||
- Added the required column `pr_id` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"pr_id" INTEGER NOT NULL,
|
||||
"timestamp" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"duration" INTEGER NOT NULL,
|
||||
"reviewed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"approved" BOOLEAN NOT NULL DEFAULT false,
|
||||
CONSTRAINT "Session_pr_id_fkey" FOREIGN KEY ("pr_id") REFERENCES "PullRequest" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Session" ("approved", "duration", "filename", "id", "reviewed", "timestamp") SELECT "approved", "duration", "filename", "id", "reviewed", "timestamp" FROM "Session";
|
||||
DROP TABLE "Session";
|
||||
ALTER TABLE "new_Session" RENAME TO "Session";
|
||||
CREATE UNIQUE INDEX "Session_pr_id_key" ON "Session"("pr_id");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- DropIndex
|
||||
DROP INDEX "Session_pr_id_key";
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[filename]` on the table `Session` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_filename_key" ON "Session"("filename");
|
||||
3
backend/migrations/migration_lock.toml
Normal file
3
backend/migrations/migration_lock.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
79
backend/requirements.txt
Normal file
79
backend/requirements.txt
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
aiofiles==24.1.0
|
||||
aiohttp==3.9.5
|
||||
aiosignal==1.3.1
|
||||
annotated-types==0.7.0
|
||||
anyio==4.4.0
|
||||
APScheduler==3.10.4
|
||||
attrs==23.2.0
|
||||
black==24.4.2
|
||||
build==1.2.1
|
||||
certifi==2024.7.4
|
||||
cffi==1.17.0
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
cryptography==43.0.0
|
||||
defusedxml==0.8.0rc2
|
||||
dnspython==2.6.1
|
||||
ecdsa==0.19.0
|
||||
email_validator==2.2.0
|
||||
fastapi==0.112.0
|
||||
fastapi-cli==0.0.4
|
||||
fastapi-oauth2==1.0.0
|
||||
fastapi-utils==0.7.0
|
||||
frozenlist==1.4.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.5
|
||||
httptools==0.6.1
|
||||
httpx==0.27.0
|
||||
idna==3.7
|
||||
Jinja2==3.1.4
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==2.1.5
|
||||
mdurl==0.1.2
|
||||
multidict==6.0.5
|
||||
mypy==1.11.0
|
||||
mypy-extensions==1.0.0
|
||||
nodeenv==1.9.1
|
||||
numpy==2.1.0
|
||||
oauthlib==3.2.2
|
||||
opencv-python==4.10.0.84
|
||||
packaging==24.1
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.2.2
|
||||
prisma==0.14.0
|
||||
psutil==5.9.8
|
||||
pyasn1==0.6.0
|
||||
pycparser==2.22
|
||||
pydantic==2.8.2
|
||||
pydantic_core==2.20.1
|
||||
Pygments==2.18.0
|
||||
PyJWT==2.9.0
|
||||
pyproject_hooks==1.1.0
|
||||
python-dotenv==1.0.1
|
||||
python-jose==3.3.0
|
||||
python-multipart==0.0.9
|
||||
python3-openid==3.2.0
|
||||
pytz==2024.1
|
||||
PyYAML==6.0.1
|
||||
requests==2.32.3
|
||||
requests-oauthlib==2.0.0
|
||||
rich==13.7.1
|
||||
rsa==4.9
|
||||
shellingham==1.5.4
|
||||
six==1.16.0
|
||||
slack_bolt==1.20.0
|
||||
slack_sdk==3.31.0
|
||||
sniffio==1.3.1
|
||||
social-auth-core==4.5.4
|
||||
starlette==0.37.2
|
||||
tomlkit==0.13.0
|
||||
typer==0.12.3
|
||||
typing-inspect==0.9.0
|
||||
typing_extensions==4.12.2
|
||||
tzlocal==5.2
|
||||
urllib3==2.2.2
|
||||
uvicorn==0.30.6
|
||||
uvloop==0.19.0
|
||||
watchfiles==0.22.0
|
||||
websockets==12.0
|
||||
yarl==1.9.4
|
||||
49
backend/schema.prisma
Normal file
49
backend/schema.prisma
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
generator client {
|
||||
provider = "prisma-client-py"
|
||||
recursive_type_depth = "5"
|
||||
interface = "asyncio"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = "file:./db/dev.db"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
created_at DateTime @default(now())
|
||||
slack_id String @unique
|
||||
name String
|
||||
pull_requests PullRequest[] @relation("PullRequestToUser")
|
||||
stream Stream?
|
||||
}
|
||||
|
||||
model Stream {
|
||||
id String @id @default(cuid())
|
||||
created_at DateTime @default(now())
|
||||
is_live Boolean @default(false)
|
||||
is_focused Boolean @default(false)
|
||||
key String @unique @default(uuid())
|
||||
user_id String @unique
|
||||
user User @relation(fields: [user_id], references: [id])
|
||||
}
|
||||
|
||||
model PullRequest {
|
||||
id Int @id @default(autoincrement())
|
||||
github_id Int @unique
|
||||
user_id String?
|
||||
gh_user_id Int
|
||||
user User? @relation("PullRequestToUser", fields: [user_id], references: [id])
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
pr_id Int
|
||||
pull PullRequest @relation(fields: [pr_id], references: [id])
|
||||
timestamp String
|
||||
filename String @unique
|
||||
duration Int // in minutes
|
||||
reviewed Boolean @default(false)
|
||||
approved Boolean @default(false)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue