import { getProxyUrl, route } from "@spacebar/api"; import { NextFunction, Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { executeWebhook } from "../../../../util/handlers/Webhook"; import { WebhookExecuteSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); const parseGitHubWebhook = (req: Request, res: Response, next: NextFunction) => { const eventType = req.headers["x-github-event"] as string; if (!eventType) { throw new HTTPError("Missing X-GitHub-Event header", 400); } const discordPayload: WebhookExecuteSchema = { username: "Github", avatar_url: "https://github.com/Github.png", }; switch (eventType) { case "commit_comment": if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] New comment on commit \`${req.body.comment.commit_id.slice(0, 7)}\``, description: req.body.comment.body.length > 500 ? `${req.body.comment.body.slice(0, 497)}...` : req.body.comment.body, url: req.body.comment.html_url, }, ]; break; case "create": discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] New ${req.body.ref_type} created: ${req.body.ref}`, }, ]; break; case "delete": discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] ${req.body.ref_type} deleted: ${req.body.ref}`, }, ]; break; case "fork": discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] Fork created: ${req.body.forkee.full_name}`, url: req.body.forkee.html_url, }, ]; break; case "issue_comment": if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, color: "pull_request" in req.body.issue ? 12576191 : 15109472, title: `[${req.body.repository.full_name}] New comment on ${"pull_request" in req.body.issue ? "pull request" : "issue"} #${req.body.issue.number}: ${ req.body.issue.title.length > 150 ? `${req.body.issue.title.slice(0, 147)}...` : req.body.issue.title }`, url: req.body.comment.html_url, description: req.body.comment.body.length > 150 ? `${req.body.comment.body.slice(0, 497)}...` : req.body.comment.body, }, ]; break; case "issues": if (!["opened", "closed"].includes(req.body.action)) { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] Issue ${req.body.action} #${req.body.issue.number}: ${req.body.issue.title}`, url: req.body.issue.html_url, }, ]; if (req.body.action === "opened") { discordPayload.embeds[0].color = 15426592; discordPayload.embeds[0].description = req.body.issue.body.length > 150 ? `${req.body.issue.body.slice(0, 147)}...` : req.body.issue.body; } break; case "member": if (req.body.action !== "added") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] New collaborator added: ${req.body.member.login}`, url: req.body.member.html_url, }, ]; break; case "public": discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] Now open sourced!`, }, ]; break; case "pull_request": // funfact: for some reason, if a PR's title is over 216 chars in length you won't see any actions taken on the PR on discord discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] Pull request ${req.body.action}: #${req.body.number} ${req.body.pull_request.title.length > 216 ? `${req.body.pull_request.title.slice(0, 213)}...` : req.body.pull_request.title}`, url: req.body.pull_request.html_url, }, ]; if (req.body.action === "opened") { discordPayload.embeds[0].description = req.body.pull_request.body.length > 500 ? `${req.body.pull_request.body.slice(0, 497)}...` : req.body.pull_request.body; discordPayload.embeds[0].color = 38912; } break; case "pull_request_review": // funfact: for some reason, if a PR's title is over 216 chars in length you won't see any actions taken on the PR on discord if (req.body.action !== "submitted") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] Pull request review submitted: #${req.body.pull_request.number} ${req.body.pull_request.title.length > 216 ? `${req.body.pull_request.title.slice(0, 213)}...` : req.body.pull_request.title}`, description: req.body.review.body.length > 500 ? `${req.body.review.body.slice(0, 497)}...` : req.body.review.body, url: req.body.review.html_url, }, ]; break; case "pull_request_review_comment": // funfact: for some reason, if a PR's title is over 216 chars in length you won't see any actions taken on the PR on discord if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, color: 12576191, title: `[${req.body.repository.full_name}] New review comment on pull request: #${req.body.pull_request.number} ${req.body.pull_request.title.length > 216 ? `${req.body.pull_request.title.slice(0, 213)}...` : req.body.pull_request.title}`, description: req.body.comment.body.length > 500 ? `${req.body.comment.body.slice(0, 497)}...` : req.body.comment.body, url: req.body.comment.html_url, }, ]; break; case "push": if (!req.body.ref.startsWith("refs/heads/")) { return; } if (req.body.forced) { discordPayload.embeds = [ { color: 16525609, author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.name}] Branch ${req.body.ref.slice(11)} was force-pushed to \`${req.body.head_commit.id.slice(0, 7)}\``, description: `[Compare changes](${req.body.compare})`, }, ]; } else { discordPayload.embeds = [ { color: 7506394, author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.name}:${req.body.ref.slice(11)}] ${req.body.commits.length} new commit${req.body.commits.length > 1 ? "s" : ""}`, url: req.body.head_commit.url, description: req.body.commits .slice(0, 5) // Discord only shows 5 first commits .map( (c: { id: string; url: string; message: string; author: { username: string } }) => `[\`${c.id.slice(0, 7)}\`](${c.url}) ${c.message.length > 46 ? `${c.message.slice(0, 47)}...` : c.message} - ${c.author.username}`, ) .join("\n"), }, ]; } break; case "release": if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] New release published: ${req.body.release.tag_name}`, url: req.body.release.html_url, }, ]; break; case "watch": if (req.body.action !== "started") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, title: `[${req.body.repository.full_name}] New star added`, url: req.body.repository.html_url, }, ]; break; case "check_run": if (req.body.action !== "completed") { return; } discordPayload.embeds = [ { color: req.body.check_run.conclusion == "success" ? 38912 : 16525609, title: `[${req.body.repository.name}] ${req.body.check_run.name} ${req.body.check_run.conclusion} on ${req.body.check_run.check_suite.head_branch}`, url: req.body.check_run.html_url, }, ]; break; case "check_suite": if (req.body.action !== "completed") { return; } discordPayload.embeds = [ { color: req.body.check_suite.conclusion == "success" ? 38912 : 16525609, title: `[${req.body.repository.name}] GitHub Actions checks ${req.body.check_suite.conclusion} on ${req.body.check_suite.head_branch}`, url: `https://github.com/${req.body.repository.full_name}/commit/${req.body.check_suite.head_commit.id}`, }, ]; break; case "discussion": if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, color: 15109472, title: `[${req.body.repository.name}] New discussion #${req.body.discussion.number}: ${req.body.discussion.title.length > 150 ? `${req.body.discussion.title.slice(0, 151)}...` : req.body.discussion.title}`, url: req.body.discussion.html_url, description: req.body.discussion.body.length > 500 ? `${req.body.discussion.body.slice(0, 497)}...` : req.body.discussion.body, }, ]; break; case "discussion_comment": if (req.body.action !== "created") { return; } discordPayload.embeds = [ { author: { name: req.body.sender.login, icon_url: req.body.sender.avatar_url, proxy_icon_url: getProxyUrl(new URL(req.body.sender.avatar_url), 80, 80), url: req.body.sender.html_url, }, color: 15109472, title: `[${req.body.comment.repository_url}] New comment on discussion #${req.body.discussion.number}: ${req.body.discussion.title.length > 150 ? `${req.body.discussion.title.slice(0, 151)}...` : req.body.discussion.title}`, url: req.body.comment.html_url, description: req.body.comment.body.length > 500 ? `${req.body.comment.body.slice(0, 497)}...` : req.body.comment.body, }, ]; break; default: return res.status(204).end(); // Yes, discord sends 204 even on invalid event } req.body = discordPayload; req.query.wait ||= "true"; next(); }; router.post( "/", parseGitHubWebhook, (req, _res, next) => { if (req.body.payload_json) { req.body = JSON.parse(req.body.payload_json); } next(); }, route({ requestBody: "WebhookExecuteSchema", query: { wait: { type: "boolean", required: false, description: "waits for server confirmation of message send before response, and returns the created message body", }, thread_id: { type: "string", required: false, description: "Send a message to the specified thread within a webhook's channel.", }, }, responses: { 204: {}, 400: { body: "APIErrorResponse", }, 404: {}, }, }), executeWebhook, ); export default router;