From f62ae6c205ca22236ca67a22c01581e6ce705816 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 4 Jul 2025 16:52:02 -0700 Subject: [PATCH] feat: remove webhooks and add invite button --- package.json | 4 - src/interfaces/GitHubPayloads.ts | 190 ----------------- src/server/github/generateCommentEmbed.ts | 5 - src/server/github/generateForkEmbed.ts | 5 - src/server/github/generateIssueEmbed.ts | 25 --- src/server/github/generatePingEmbed.ts | 5 - src/server/github/generatePullEmbed.ts | 17 -- src/server/github/generateStarEmbed.ts | 8 - src/server/serve.ts | 244 +--------------------- 9 files changed, 3 insertions(+), 500 deletions(-) delete mode 100644 src/interfaces/GitHubPayloads.ts delete mode 100644 src/server/github/generateCommentEmbed.ts delete mode 100644 src/server/github/generateForkEmbed.ts delete mode 100644 src/server/github/generateIssueEmbed.ts delete mode 100644 src/server/github/generatePingEmbed.ts delete mode 100644 src/server/github/generatePullEmbed.ts delete mode 100644 src/server/github/generateStarEmbed.ts diff --git a/package.json b/package.json index cb7d63a..65f8493 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,6 @@ "bugs": { "url": "https://github.com/nhcarrigan/mod-bot/issues" }, - "engines": { - "node": "22", - "pnpm": "10" - }, "homepage": "https://github.com/nhcarrigan/mod-bot#readme", "devDependencies": { "@nhcarrigan/eslint-config": "3.2.0", diff --git a/src/interfaces/GitHubPayloads.ts b/src/interfaces/GitHubPayloads.ts deleted file mode 100644 index 180aa41..0000000 --- a/src/interfaces/GitHubPayloads.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * The structure of the NESTED issue data from the GitHub Webhook. - */ -interface GithubIssuePayload { - id: number; - node_id: string; - url: string; - repository_url: string; - html_url: string; - number: number; - state: string; - state_reason: string | null; - title: string; - body: string; - user: GithubUserPayload; - created_at: string; - updated_at: string; - closed_by: GithubUserPayload; -} - -interface GithubPullRequestPayload { - html_url: string; - body: string; - number: number; - merged: boolean; - title: string; - user: GithubUserPayload; -} - -/** - * Structure of the repo data, sent on pretty much - * every GitHub Webhook payload. - */ -interface GithubRepoPayload { - id: number; - node_id: string; - name: string; - full_name: string; - owner: GithubUserPayload; - private: boolean; - html_url: string; - description: string; - fork: boolean; - url: string; - archive_url: string; - assignees_url: string; - blobs_url: string; - branches_url: string; - collaborators_url: string; - comments_url: string; - commits_url: string; - compare_url: string; - contents_url: string; - contributors_url: string; - deployments_url: string; - downloads_url: string; - events_url: string; - forks_url: string; - git_commits_url: string; - git_refs_url: string; - git_tags_url: string; - git_url: string; - issue_comment_url: string; - issue_events_url: string; - issues_url: string; - keys_url: string; - labels_url: string; - languages_url: string; - merges_url: string; - milestones_url: string; - notifications_url: string; - pulls_url: string; - releases_url: string; - ssh_url: string; - stargazers_url: string; - statuses_url: string; - subscribers_url: string; - subscription_url: string; - tags_url: string; - teams_url: string; - trees_url: string; - clone_url: string; - mirror_url: string; - hooks_url: string; - svn_url: string; - homepage: string; - language: string | null; - forks: number; - forks_count: number; - stargazers_count: number; - watchers_count: number; - watchers: number; - size: number; - default_branch: string; - open_issues_count: number; - open_issues: number; - created_at: string; -} - -interface GithubUserPayload { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; - name: string; - company: string; - blog: string; - location: string; - email: string; - hireable: boolean; -} - -/** - * The structure of the comment data from the Github Webhook. - */ -export interface GithubCommentPayload { - action: string; - issue: GithubIssuePayload; - comment: { - html_url: string; - body: string; - user: GithubUserPayload; - }; - repository: GithubRepoPayload; - sender: GithubUserPayload; -} - -export interface GithubForkPayload { - forkee: GithubRepoPayload; - repository: GithubRepoPayload; - sender: GithubUserPayload; -} - -/** - * The structure of the top level issue data from the GitHub webhook. - */ -export interface GithubIssuesPayload { - action: string; - issue: GithubIssuePayload; - repository: GithubRepoPayload; - sender: GithubUserPayload; -} - -/** - * The structure of the ping payload when a new GitHub webhook - * is initialised. - */ -export interface GithubPingPayload { - zen: string; - hook_id: string; - hook: Record; - repository: GithubRepoPayload; - organization: Record; - sender: GithubUserPayload; -} - -/** - * Structure of the pull request data from the GitHub Webhook. - */ -export interface GithubPullPayload { - action: string; - number: number; - pull_request: GithubPullRequestPayload; - repository: GithubRepoPayload; - sender: GithubUserPayload; -} - -/** - * Structure of the star data sent from the GitHub Webhook. - */ -export interface GithubStarPayload { - action: "created" | "deleted"; - starred_at: string; - repository: GithubRepoPayload; - sender: GithubUserPayload; -} diff --git a/src/server/github/generateCommentEmbed.ts b/src/server/github/generateCommentEmbed.ts deleted file mode 100644 index 495b202..0000000 --- a/src/server/github/generateCommentEmbed.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GithubCommentPayload } from "../../interfaces/GitHubPayloads"; - -export const generateCommentEmbed = (data: GithubCommentPayload): string => { - return `[New comment detected on ${data.repository.name}#${data.issue.number}.](<${data.comment.html_url}>)`; -}; diff --git a/src/server/github/generateForkEmbed.ts b/src/server/github/generateForkEmbed.ts deleted file mode 100644 index c56cad4..0000000 --- a/src/server/github/generateForkEmbed.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GithubForkPayload } from "../../interfaces/GitHubPayloads"; - -export const generateForkEmbed = (data: GithubForkPayload): string => { - return `[New fork detected - ${data.repository.name}](<${data.forkee.html_url}>)`; -}; diff --git a/src/server/github/generateIssueEmbed.ts b/src/server/github/generateIssueEmbed.ts deleted file mode 100644 index b3851b9..0000000 --- a/src/server/github/generateIssueEmbed.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GithubIssuesPayload } from "../../interfaces/GitHubPayloads"; - -export const generateIssuesEmbed = ( - data: GithubIssuesPayload -): string | null => { - if (!["opened", "edited", "closed"].includes(data.action)) { - return null; - } - if (data.action === "closed") { - if (data.issue.state_reason === "completed") { - return `[Issue closed as complete - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; - } - if (data.issue.state_reason === "not_planned") { - return `[Issue closed as not planned - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; - } - return `[Issue closed - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; - } - if (data.issue.state_reason === "reopened") { - return `[Issue reopened - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; - } - if (data.action === "edited") { - return `[Issue updated - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; - } - return `[New issue created - ${data.repository.name}#${data.issue.number}](<${data.issue.html_url}>)`; -}; diff --git a/src/server/github/generatePingEmbed.ts b/src/server/github/generatePingEmbed.ts deleted file mode 100644 index c4c2e5a..0000000 --- a/src/server/github/generatePingEmbed.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GithubPingPayload } from "../../interfaces/GitHubPayloads"; - -export const generatePingEmbed = (data: GithubPingPayload): string => { - return `[Now watching ${data.repository.name}](<${data.repository.url}>)`; -}; diff --git a/src/server/github/generatePullEmbed.ts b/src/server/github/generatePullEmbed.ts deleted file mode 100644 index 9cd4f63..0000000 --- a/src/server/github/generatePullEmbed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { GithubPullPayload } from "../../interfaces/GitHubPayloads"; - -export const generatePullEmbed = (data: GithubPullPayload): string | null => { - if (!["opened", "edited", "closed"].includes(data.action)) { - return null; - } - if (data.pull_request.merged) { - return `[Pull request merged - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; - } - if (data.action === "edited") { - return `[Pull request updated - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; - } - if (data.action === "closed") { - return `[Pull request closed - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; - } - return `[New pull request - ${data.repository.name}#${data.pull_request.number}](<${data.pull_request.html_url}>)`; -}; diff --git a/src/server/github/generateStarEmbed.ts b/src/server/github/generateStarEmbed.ts deleted file mode 100644 index b9ac61c..0000000 --- a/src/server/github/generateStarEmbed.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { GithubStarPayload } from "../../interfaces/GitHubPayloads"; - -export const generateStarEmbed = (data: GithubStarPayload): string | null => { - if (data.action !== "created") { - return null; - } - return `[New stargazer! ${data.repository.name}](<${data.repository.html_url}>)`; -}; diff --git a/src/server/serve.ts b/src/server/serve.ts index 399f35a..4ce1fc3 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -1,23 +1,11 @@ -import { createHmac, timingSafeEqual } from "crypto"; import { readFile } from "fs/promises"; import http from "http"; import https from "https"; -import { Octokit } from "@octokit/rest"; -import { GuildTextBasedChannel } from "discord.js"; import express from "express"; import { register } from "prom-client"; -import { IgnoredActors, ThankYou } from "../config/Github"; import { ExtendedClient } from "../interfaces/ExtendedClient"; -import { errorHandler } from "../utils/errorHandler"; - -import { generateCommentEmbed } from "./github/generateCommentEmbed"; -import { generateForkEmbed } from "./github/generateForkEmbed"; -import { generateIssuesEmbed } from "./github/generateIssueEmbed"; -import { generatePingEmbed } from "./github/generatePingEmbed"; -import { generatePullEmbed } from "./github/generatePullEmbed"; -import { generateStarEmbed } from "./github/generateStarEmbed"; /** * Instantiates the web server for GitHub webhooks. @@ -25,24 +13,7 @@ import { generateStarEmbed } from "./github/generateStarEmbed"; * @param {ExtendedClient} bot The bot's Discord instance. */ export const serve = async (bot: ExtendedClient) => { - const githubSecret = process.env.GITHUB_WEBHOOK_SECRET; - const patreonSecret = process.env.PATREON_WEBHOOK_SECRET; - const kofiSecret = process.env.KOFI_WEBHOOK_SECRET; - const token = process.env.GITHUB_TOKEN; - if (!githubSecret || !token || !kofiSecret || !patreonSecret) { - await bot.env.debugHook.send({ - content: "Missing necessary secrets. Web server will not be started.", - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - return; - } const app = express(); - app.post("/patreon", express.text({ type: "*/*" })); - app.use(express.urlencoded({ extended: true })); - app.use(express.json()); app.get("/", (_req, res) => { res.send(` @@ -61,6 +32,9 @@ export const serve = async (bot: ExtendedClient) => { Celestine

A paid moderation bot for Discord.

+

Links

@@ -95,218 +69,6 @@ export const serve = async (bot: ExtendedClient) => { } }); - app.post("/kofi", async (req, res) => { - const payload = JSON.parse(req.body.data); - const { - verification_token: verifyToken, - from_name: fromName, - is_subscription_payment: isSub, - is_first_subscription_payment: isFirstSub - } = payload; - if (!verifyToken) { - await bot.env.debugHook.send({ - content: - "Received request with no signature.\n\n" + - JSON.stringify(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(400).send("Invalid payload."); - return; - } - if (verifyToken !== kofiSecret) { - await bot.env.debugHook.send({ - content: - "Received request with bad signature.\n\n" + - JSON.stringify(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(403).send("Invalid signature."); - return; - } - res.status(200).send("Valid signature found!"); - - // ignore recurring subscriptions - if (isSub && !isFirstSub) { - return; - } - - const channel = (await bot.channels.fetch( - "1235114666322034790" - )) as GuildTextBasedChannel; - - await channel.send({ - content: `## Big thanks to ${fromName} for sponsoring us on KoFi!\n\nTo claim your sponsor role, please DM Naomi with your KoFi receipt.` - }); - }); - - app.post("/patreon", async (req, res) => { - // validate headers - const header = req.headers["x-patreon-signature"]; - if (!header) { - await bot.env.debugHook.send({ - content: - "Received request with no signature.\n\n" + - String(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(403).send("No valid signature present."); - return; - } - const hash = createHmac("MD5", patreonSecret) - .update(req.body) - .digest("hex"); - if (hash !== header) { - await bot.env.debugHook.send({ - content: - "Received request with bad signature.\n\n" + - String(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(403).send("Signature is not correct."); - return; - } - res.status(200).send("Signature is correct."); - - const event = req.headers["x-patreon-event"]; - - if (event !== "pledges:create") { - return; - } - - const obj = JSON.parse(req.body); - - const user = obj.included.find( - (obj: Record) => obj.type === "user" - ); - - const channel = (await bot.channels.fetch( - "1235114666322034790" - )) as GuildTextBasedChannel; - - await channel?.send({ - content: `## Big thanks to ${user.attributes.full_name} for sponsoring us on Patreon!\n\nTo claim your sponsor role, please DM Naomi with your patreon receipt.` - }); - }); - - app.post("/github", async (req, res) => { - try { - const header = req.headers["x-hub-signature-256"]; - if (!header || Array.isArray(header)) { - await bot.env.debugHook.send({ - content: - "Received request with no signature.\n\n" + - JSON.stringify(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(403).send("No valid signature present."); - return; - } - const signature = createHmac("sha256", githubSecret) - .update(JSON.stringify(req.body)) - .digest("hex"); - const trusted = Buffer.from(`sha256=${signature}`, "ascii"); - const sent = Buffer.from(header, "ascii"); - const safe = timingSafeEqual(trusted, sent); - if (!safe) { - await bot.env.debugHook.send({ - content: - "Received request with bad signature.\n\n" + - JSON.stringify(req.body).slice(0, 1500), - username: bot.user?.username ?? "bot", - avatarURL: - bot.user?.displayAvatarURL() ?? - "https://cdn.nhcarrigan.com/avatars/nhcarrigan.png" - }); - res.status(403).send("Signature is not correct."); - return; - } - res.status(200).send("Signature is correct."); - - const event = req.headers["x-github-event"] as string; - if (event === "sponsorship" && req.body.action === "created") { - const channel = (await bot.channels.fetch( - "1235114666322034790" - )) as GuildTextBasedChannel; - await channel?.send({ - content: `## Big thanks to ${req.body.sponsorship.sponsor.login} for sponsoring us on GitHub!\n\nTo claim your sponsor role, please make sure your GitHub account is connected to your Discord account, then ping Mama Naomi for your role!` - }); - } - - if ( - IgnoredActors.includes( - req.body.pull_request?.user.login || req.body.sender?.login - ) - ) { - return; - } - - const embedGenerator = { - ping: generatePingEmbed, - star: generateStarEmbed, - issues: generateIssuesEmbed, - pull_request: generatePullEmbed, - issue_comment: generateCommentEmbed, - fork: generateForkEmbed - }; - - const isValidKey = ( - event: string - ): event is keyof typeof embedGenerator => { - return event in embedGenerator; - }; - - const content = isValidKey(event) - ? embedGenerator[event](req.body) - : null; - - if (content) { - const channel = (await bot.channels.fetch( - "1231028190403891212" - )) as GuildTextBasedChannel; - - await channel?.send({ - content - }); - } - - if (event === "pull_request") { - const owner = req.body.repository.owner.login; - const repo = req.body.repository.name; - const number = req.body.number; - const isMerged = - req.body.action === "closed" && req.body.pull_request.merged; - const github = new Octokit({ - auth: process.env.GITHUB_TOKEN - }); - if (isMerged && req.body.pull_request?.user.login !== "naomi-lgbt") { - await github.issues.createComment({ - owner, - repo, - issue_number: number, - body: ThankYou - }); - } - } - } catch (err) { - await errorHandler(bot, "github webhook", err); - } - }); - const httpServer = http.createServer(app); httpServer.listen(9080, async () => {