50% done with submission flow i think

This commit is contained in:
Micha Albert 2024-08-17 22:25:32 +00:00
parent f57c38250a
commit 25dbca5c56
7 changed files with 218 additions and 44 deletions

View file

@ -1,3 +1,5 @@
import hashlib
import hmac
import json import json
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@ -5,21 +7,16 @@ from random import choice
from secrets import token_hex from secrets import token_hex
from typing import Dict, List from typing import Dict, List
import fastapi
import httpx import httpx
import uvicorn
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI, Request, Response from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi_oauth2.claims import Claims
from fastapi_oauth2.client import OAuth2Client
from fastapi_oauth2.config import OAuth2Config
from prisma import Prisma from prisma import Prisma
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slack_bolt.async_app import AsyncAck, AsyncApp from slack_bolt.async_app import AsyncAck, AsyncApp
from social_core.backends.slack import SlackOAuth2
import uvicorn
load_dotenv(dotenv_path="./.env") load_dotenv(dotenv_path="./.env")
@ -28,19 +25,27 @@ active_streams: List[Dict[str, str | bool]] = []
scheduler = AsyncIOScheduler() scheduler = AsyncIOScheduler()
oauth2_config = OAuth2Config(
allow_http=False, def verify_gh_signature(payload_body, secret_token, signature_header):
jwt_secret=os.environ["JWT_SECRET"], """Verify that the payload was sent from GitHub by validating SHA256.
jwt_expires=os.environ["JWT_EXPIRES"],
jwt_algorithm=os.environ["JWT_ALGORITHM"], Raise and return 403 if not authorized.
clients=[
OAuth2Client( Args:
backend=SlackOAuth2, payload_body: original request body to verify (request.body())
client_id=os.environ["SLACK_TOKEN"], secret_token: GitHub app webhook token (WEBHOOK_SECRET)
client_secret=os.environ["SLACK_SIGNING_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 update_active(): async def update_active():
@ -75,17 +80,13 @@ async def update_active():
) )
new_stream = choice(active_streams) new_stream = choice(active_streams)
print(f"found new stream to make active: {new_stream}") print(f"found new stream to make active: {new_stream}")
try:
await db.connect()
except Exception as e:
print(e)
print(f"trying to find user associated with stream {active_stream['name']}") print(f"trying to find user associated with stream {active_stream['name']}")
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 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 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 = 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 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 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
await db.disconnect() return True
async def check_for_new(): async def check_for_new():
@ -124,19 +125,16 @@ async def lifespan(app: FastAPI):
scheduler.start() scheduler.start()
scheduler.add_job(update_active, IntervalTrigger(seconds=5 * 60)) scheduler.add_job(update_active, IntervalTrigger(seconds=5 * 60))
scheduler.add_job(check_for_new, IntervalTrigger(seconds=3)) scheduler.add_job(check_for_new, IntervalTrigger(seconds=3))
try: await db.connect()
await db.connect()
except Exception:
pass
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for stream in await db.stream.find_many(): for stream in await db.stream.find_many():
await client.post( await client.post(
"http://127.0.0.1:9997/v3/config/paths/add/" + stream.key, "http://127.0.0.1:9997/v3/config/paths/add/" + stream.key,
json={"name": stream.key}, json={"name": stream.key},
) )
await db.disconnect()
yield yield
scheduler.shutdown() scheduler.shutdown()
await db.disconnect()
api = FastAPI(lifespan=lifespan) # type: ignore api = FastAPI(lifespan=lifespan) # type: ignore
@ -157,11 +155,48 @@ bolt = AsyncApp(
bolt_handler = AsyncSlackRequestHandler(bolt) bolt_handler = AsyncSlackRequestHandler(bolt)
@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:
print("Added label has same id as OBL label!")
async with httpx.AsyncClient() as client:
db_pr = await db.pullrequest.create({"github_id": body["number"]})
db_pr_token = db_pr.token
await client.post(
f"https://api.github.com/repos/hackclub/OnBoard/issues/{body["issue"]["number"]}/comments",
headers={
"Authorization": f"token {os.environ['GH_TOKEN']}",
"Accept": "application/vnd.github.v3+json",
},
json={
"body": f"Hey, I'm Micha, a.k.a `@mra` on Slack! It looks like this is an OnBoard Live submission. If that sounds right, then go to the #onboard-live channel on Slack and send the message `/onboard-live-submit {db_pr_token}`. Doing that helps us link this pull request to your Slack account lets you select your sessions for review.\n###### If you have no clue what OnBoard Live is, please disregard this automated message!"
},
)
elif "created" in body and "comment" in body:
if body["comment"]["user"]["id"] == body["issue"]["user"]["id"]:
db_pr = await db.pullrequest.find_first(where={"github_id": body["issue"]["number"]})
if db_pr:
if db_pr.possible_users:
for user in db_pr.possible_users:
if hashlib.sha256(bytes(f"{db_pr.secondary_token}+{user.slack_id}", encoding="utf-8")).hexdigest() in body["comment"]["body"]:
# Yay, the user who ran the Slack submit command is the same user who submitted the PR!
db_pr.user = user
break
else:
print("possible users was none")
return
@api.get("/api/v1/stream_key/{stream_key}") @api.get("/api/v1/stream_key/{stream_key}")
async def get_stream_by_key(stream_key: str): async def get_stream_by_key(stream_key: str):
await db.connect()
stream = await db.stream.find_first(where={"key": stream_key}) stream = await db.stream.find_first(where={"key": stream_key})
await db.disconnect()
return ( return (
stream if stream else Response(status_code=404, content="404: Stream not found") stream if stream else Response(status_code=404, content="404: Stream not found")
) )
@ -220,10 +255,6 @@ async def deny(ack, body):
@bolt.action("approve") @bolt.action("approve")
async def approve(ack, body): async def approve(ack, body):
await ack() await ack()
try:
await db.connect()
except Exception:
pass
message = body["message"] message = body["message"]
applicant_slack_id = message["blocks"][len(message) - 3]["text"]["text"].split( applicant_slack_id = message["blocks"][len(message) - 3]["text"]["text"].split(
": " ": "
@ -262,7 +293,6 @@ async def approve(ack, body):
channel=sumbitter_convo["channel"]["id"], 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", 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",
) )
await db.disconnect()
@bolt.view("apply") @bolt.view("apply")
@ -287,9 +317,6 @@ async def handle_application_submission(ack, body):
channel=sumbitter_convo["channel"]["id"], 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 ''}", 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 ''}",
) )
admin_convo = await bolt.client.conversations_open(
users=os.environ["ADMIN_SLACK_ID"], return_im=True
)
will_behave = True will_behave = True
# boxes = body["view"]["state"]["values"]["kAgeY"]["checkboxes"]["selected_options"] # boxes = body["view"]["state"]["values"]["kAgeY"]["checkboxes"]["selected_options"]
# if len(boxes) == 1 and boxes[0]["value"] == "value-1": # if len(boxes) == 1 and boxes[0]["value"] == "value-1":
@ -376,6 +403,29 @@ async def handle_application_submission(ack, body):
) )
@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={"token": text})
db_user = await db.user.find_first_or_raise(where={"slack_id": user_id})
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 token! If this seems like a mistake, please message <@U05C64XMMHV> about it!",
)
return
await db.pullrequest.update(where={"id": db_pr.id}, data={"possible_users": {"set": [{"id": db_user.id}]}})
await bolt.client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text=f"Please go to <https://github.com/hackclub/OnBoard/pull/{db_pr.github_id}|your pull request> and add a comment containing the secret code `{hashlib.sha256(bytes(f"{db_pr.secondary_token}+{user_id}", encoding="utf-8")).hexdigest()}`. This helps us make sure this is your PR!",
)
@bolt.command("/onboard-live-apply") @bolt.command("/onboard-live-apply")
async def apply(ack: AsyncAck, command): async def apply(ack: AsyncAck, command):
await ack() await ack()

View 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");

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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");

View file

@ -10,11 +10,13 @@ datasource db {
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
created_at DateTime @default(now()) created_at DateTime @default(now())
slack_id String @unique slack_id String @unique
name String name String
stream Stream? stream Stream?
possible_pulls PullRequest[] @relation(name: "PullRequestToPossibleUser") // pull requests that this user has tried to claim via the /onboard-live-submit command on Slack
known_pulls PullRequest[] @relation(name: "PullRequestToKnownUser") // pull requests that have been verified to belong to this user
} }
model Stream { model Stream {
@ -26,3 +28,13 @@ model Stream {
user_id String @unique user_id String @unique
user User @relation(fields: [user_id], references: [id]) user User @relation(fields: [user_id], references: [id])
} }
model PullRequest {
id Int @id @default(autoincrement())
github_id Int @unique
user User? @relation(name: "PullRequestToKnownUser", fields: [known_user_id], references: [id])
known_user_id String?
token String @unique @default(uuid())
secondary_token String @unique @default(uuid())
possible_users User[] @relation(name: "PullRequestToPossibleUser")
}