diff --git a/github-bot/.gitignore b/github-bot/.gitignore new file mode 100644 index 000000000..c1fa1ef44 --- /dev/null +++ b/github-bot/.gitignore @@ -0,0 +1,2 @@ +js +!example/.env \ No newline at end of file diff --git a/github-bot/README.md b/github-bot/README.md new file mode 100644 index 000000000..38ac13037 --- /dev/null +++ b/github-bot/README.md @@ -0,0 +1,4 @@ +# Tachi GitHub Bot + +We use this to listen for pull requests that affect the Database Seeds. +When this happens, we provide a link for viewing the diff. Simple! \ No newline at end of file diff --git a/github-bot/example/.env b/github-bot/example/.env new file mode 100644 index 000000000..631bab08c --- /dev/null +++ b/github-bot/example/.env @@ -0,0 +1,4 @@ +APP_ID=1000 +WEBHOOK_SECRET=changeme +PORT=80 +CLIENT_SECRET=changeme diff --git a/github-bot/package.json b/github-bot/package.json new file mode 100644 index 000000000..4bd1cf251 --- /dev/null +++ b/github-bot/package.json @@ -0,0 +1,34 @@ +{ + "name": "tachi-github-bot", + "version": "1.0.0", + "private": true, + "description": "A GitHub bot for listening to Tachi's GitHub Repo.", + "author": "zkldi", + "license": "ISC", + "homepage": "https://github.com//", + "keywords": [ + "probot", + "github", + "probot-app" + ], + "scripts": { + "build": "tsc", + "start": "probot run ./lib/index.js" + }, + "dependencies": { + "@octokit/webhooks": "10.1.5", + "dotenv": "16.0.0", + "express": "4.17.2", + "node-fetch": "2.6.7", + "prudence": "0.9.8" + }, + "devDependencies": { + "@types/express": "4.17.13", + "@types/node-fetch": "2.5.12", + "@types/node": "^16.0.0", + "typescript": "4.5.5" + }, + "engines": { + "node": ">= 16.0.0" + } +} \ No newline at end of file diff --git a/github-bot/src/config.ts b/github-bot/src/config.ts new file mode 100644 index 000000000..ae693859e --- /dev/null +++ b/github-bot/src/config.ts @@ -0,0 +1,31 @@ +import "dotenv"; +import p from "prudence"; + +function ParseEnvVars() { + const err = p( + process.env, + { + APP_ID: "string", + WEBHOOK_SECRET: "string", + PORT: (self) => + p.isPositiveInteger(Number(self)) === true || + "Should be a string representing a whole integer port.", + CLIENT_SECRET: "string", + }, + {}, + { allowExcessKeys: true } + ); + + if (err) { + throw new Error(err.message); + } + + return { + appId: process.env.APP_ID!, + webhookSecret: process.env.WEBHOOK_SECRET!, + port: process.env.PORT!, + clientSecret: process.env.CLIENT_SECRET!, + }; +} + +export const ProcessEnv = ParseEnvVars(); diff --git a/github-bot/src/index.ts b/github-bot/src/index.ts new file mode 100644 index 000000000..2638ab85a --- /dev/null +++ b/github-bot/src/index.ts @@ -0,0 +1,127 @@ +/* eslint-disable no-console */ +import { ProcessEnv } from "./config"; +import express from "express"; +import fetch from "node-fetch"; +import { URLSearchParams } from "url"; +import type { EmitterWebhookEvent } from "@octokit/webhooks"; +import type { Express } from "express"; + +export const app: Express = express(); + +app.use(express.json()); + +// Let NGINX work its magic. +app.set("trust proxy", "loopback"); + +// Disable query string nesting such as ?a[b]=4 -> {a: {b: 4}}. This +// almost always results in a painful security vuln. +app.set("query parser", "simple"); + +/** + * Return the status of this bot and the version it's running. + * + * @name GET / + */ +app.get("/", (req, res) => + res.status(200).json({ + success: true, + description: "Github Bot is online!", + body: { + time: Date.now(), + }, + }) +); + +/** + * Create a response that contains a link to the seeds diff viewer. + */ +function mkSeedDiffViewMsg(repo: string, sha: string, compareRepo: string, compareSHA: string) { + const params = new URLSearchParams({ + repo: ConvertGitHubURL(repo), + sha, + compareRepo: ConvertGitHubURL(compareRepo), + compareSHA, + }); + + return `Beep Boop! This change affects \`database-seeds/collections\`. We have a dedicated diff-viewer for this part of the codebase, which makes it easier to view changes. + +***** + +[View Seeds Diff](https://bokutachi.xyz/dashboard/utils/seeds?${params.toString()} +`; +} + +function ConvertGitHubURL(url: string) { + return url.replace("https://github.com/", "GitHub:"); +} + +/** + * Listens for GitHub webhook calls. + * + * @name POST /webhook + */ +app.post("/webhook", async (req, res) => { + console.dir(req.body); + + const event = req.header("X-GitHub-Event"); + + if (event !== "pull_request") { + return res.status(400).json({ + success: false, + description: `Unsupported Event.`, + }); + } + + const body = req.body as EmitterWebhookEvent<"pull_request">["payload"]; + + if (body.action !== "opened" && body.action !== "edited") { + return res.status(400).json({ + success: false, + description: `We only care about opened/edited pull requests!`, + }); + } + + const filesChanged = (await fetch( + `https://api.github.com/repos/TNG-dev/Tachi/pulls/${body.number}/files` + ).then((r) => r.json())) as Array<{ filename: string }>; + + // if any file modified in this pr is a collection + if (filesChanged.some((k) => k.filename.startsWith("database-seeds/collections"))) { + // post a link to the diff viewer in the PR comments. + await fetch(body.pull_request._links.comments.href, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ProcessEnv.clientSecret}`, + }, + body: JSON.stringify({ + body: mkSeedDiffViewMsg( + body.pull_request.head.repo.url, + body.pull_request.head.sha, + body.pull_request.base.repo.url, + body.pull_request.base.sha + ), + }), + }); + } + + return res.status(200).json({ + success: true, + description: "Handled request.", + body: {}, + }); +}); + +/** + * 404 Handler. If something gets to this point, they haven't matched with anything. + * + * @name ALL * + */ +app.all("*", (req, res) => + res.status(404).json({ + success: false, + description: "Nothing found here.", + }) +); + +console.log(`Starting express server on port ${ProcessEnv.port}.`); diff --git a/github-bot/tsconfig.json b/github-bot/tsconfig.json new file mode 100644 index 000000000..2c5d739a3 --- /dev/null +++ b/github-bot/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": [ + "es2015", + "es2017" + ], + "outDir": "js/" + }, + "include": [ + "src/" + ], +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bb5d7675..c4f6115e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,6 +276,29 @@ importers: prudence: 0.9.8 xml2js: 0.4.23 + github-bot: + specifiers: + '@octokit/webhooks': 10.1.5 + '@types/express': 4.17.13 + '@types/node': 16.11.7 + '@types/node-fetch': 2.5.12 + dotenv: 16.0.0 + express: 4.17.2 + node-fetch: 2.6.7 + prudence: 0.9.8 + typescript: 4.5.5 + dependencies: + '@octokit/webhooks': 10.1.5 + dotenv: 16.0.0 + express: 4.17.2 + node-fetch: 2.6.7 + prudence: 0.9.8 + devDependencies: + '@types/express': 4.17.13 + '@types/node': 16.11.7 + '@types/node-fetch': 2.5.12 + typescript: 4.5.5 + server: specifiers: '@aws-sdk/client-s3': 3.49.0 @@ -3030,6 +3053,44 @@ packages: fastq: 1.13.0 dev: true + /@octokit/openapi-types/13.12.0: + resolution: {integrity: sha512-1QYzZrwnn3rTQE7ZoSxXrO8lhu0aIbac1c+qIPOPEaVXBWSaUyLV1x9yt4uDQOwmu6u5ywVS8OJgs+ErDLf6vQ==} + dev: false + + /@octokit/request-error/3.0.1: + resolution: {integrity: sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==} + engines: {node: '>= 14'} + dependencies: + '@octokit/types': 7.5.1 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/types/7.5.1: + resolution: {integrity: sha512-Zk4OUMLCSpXNI8KZZn47lVLJSsgMyCimsWWQI5hyjZg7hdYm0kjotaIkbG0Pp8SfU2CofMBzonboTqvzn3FrJA==} + dependencies: + '@octokit/openapi-types': 13.12.0 + dev: false + + /@octokit/webhooks-methods/3.0.0: + resolution: {integrity: sha512-FAIyAchH9JUKXugKMC17ERAXM/56vVJekwXOON46pmUDYfU7uXB4cFY8yc8nYr5ABqVI7KjRKfFt3mZF7OcyUA==} + engines: {node: '>= 14'} + dev: false + + /@octokit/webhooks-types/6.3.6: + resolution: {integrity: sha512-x6yBtWobk20OhOiJ4VWsH3iJ/30IG+VoDWSgS4Tiyidi2KOiBS3bL+AJrNuq4OyNuWOM/FbHQTp6KEZs1oPD/g==} + dev: false + + /@octokit/webhooks/10.1.5: + resolution: {integrity: sha512-sQkxM6l9HdG1vsHFj2T/o8SnCPDDxovcs0rsSd4UR5jJFNPCPIBRmFNVHfM37nncLKuTwIpmMeePphNf1k6Waw==} + engines: {node: '>= 14'} + dependencies: + '@octokit/request-error': 3.0.1 + '@octokit/webhooks-methods': 3.0.0 + '@octokit/webhooks-types': 6.3.6 + aggregate-error: 3.1.0 + dev: false + /@popperjs/core/2.11.5: resolution: {integrity: sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==} dev: false @@ -3342,7 +3403,6 @@ packages: dependencies: '@types/node': 16.11.7 form-data: 3.0.1 - dev: false /@types/node/16.11.7: resolution: {integrity: sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==} @@ -3822,7 +3882,6 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true /ajv-errors/1.0.1_ajv@6.12.6: resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} @@ -4930,7 +4989,6 @@ packages: /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true /cli-cursor/2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} @@ -5916,6 +5974,10 @@ packages: engines: {node: '>= 0.8'} dev: false + /deprecation/2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + /dequal/2.0.2: resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==} engines: {node: '>=6'} @@ -7346,7 +7408,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -8126,7 +8187,6 @@ packages: /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - dev: true /indexes-of/1.0.1: resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1a3a44940..597bcad4e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,3 +7,4 @@ packages: - "database-seeds/**" - "server/**" - "sieglinde/**" + - "github-bot/**"