feat: remove webhooks and add invite button
Code Analysis / SonarQube (push) Failing after 16s
Node.js CI / Lint and Test (push) Successful in 42s

This commit is contained in:
2025-07-04 16:52:02 -07:00
parent 29d0a25073
commit f62ae6c205
9 changed files with 3 additions and 500 deletions
@@ -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}>)`;
};
-5
View File
@@ -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}>)`;
};
-25
View File
@@ -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}>)`;
};
-5
View File
@@ -1,5 +0,0 @@
import { GithubPingPayload } from "../../interfaces/GitHubPayloads";
export const generatePingEmbed = (data: GithubPingPayload): string => {
return `[Now watching ${data.repository.name}](<${data.repository.url}>)`;
};
-17
View File
@@ -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}>)`;
};
-8
View File
@@ -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}>)`;
};
+3 -241
View File
@@ -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) => {
<img src="https://cdn.nhcarrigan.com/new-avatars/celestine-full.png" width="250" alt="Celestine" />
<section>
<p>A paid moderation bot for Discord.</p>
<a href="https://discord.com/oauth2/authorize?client_id=1235128719836712970&permissions=8&integration_type=0&scope=bot+applications.commands" class="social-button discord-button" style="display: inline-block; background-color: #5865F2; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin: 5px;">
<i class="fab fa-discord"></i> Add to Discord
</a>
</section>
<section>
<h2>Links</h2>
@@ -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<string, string>) => 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 () => {