Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ DISCORD_PROD_SERVER_ID="528728562780501729853789"
DISCORD_PROD_VERIFY_CHANNEL_ID="5287378262780501729853789"
DISCORD_SECRET_TOKEN="FHBAHJABDhbHSUeyi7398.S72ljd.HKBVAfug73gifa-74gfwiyB-BHJBDHV07hAJf83"
HK_ENV="development"
INTERNAL_AUTH_KEY="bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-fbnauib4783-bhfbsjfbs"
SHARED_SECRET="bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-fbnauib4783-bhfbsjfbs"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_YRGIBHSBIbabffjdhvbuYGI7BK"
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
Expand Down
1 change: 1 addition & 0 deletions apps/bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist
255 changes: 22 additions & 233 deletions apps/bot/bot.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { Client, Collection, GatewayIntentBits } from "discord.js";
import {
Client,
Collection,
Events,
GatewayIntentBits,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
} from "discord.js";
import { readdirSync } from "node:fs";
import path from "node:path";
import { Hono } from "hono";
import { serve } from "bun";
import c from "config";
import { db } from "db";
import { eq } from "db/drizzle";
import { discordVerification } from "db/schema";
import { getHacker } from "db/functions";
import { nanoid } from "nanoid";
loadCommands,
loadEvents,
loadInteractions,
loadReceivers,
} from "./utils/loaders";
import express from "express";
import sharedSecretMiddleware from "./middleware";

/* DISCORD BOT */

Expand All @@ -31,224 +20,24 @@ const client = new Client({
});

client.commands = new Collection();
(client as any).interactions = new Collection();

const commandsPath = path.join(__dirname, "commands");
const commandFiles = readdirSync(commandsPath).filter((file) =>
file.endsWith(".ts"),
);
for (const file of commandFiles) {
console.log(`[Loading Command] ${file}`);
const filePath = path.join(commandsPath, file);
const command = require(filePath);
// Set a new item in the Collection with the key as the command name and the value as the exported module
if ("data" in command && "execute" in command) {
client.commands.set(command.data.name, command);
} else {
console.log(
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`,
);
}
}
console.log(`Loaded ${client.commands.size} Commands`);
// Load commands and events and interactions from according folders.
loadCommands(client);
loadEvents(client);
loadInteractions(client);

client.on(Events.InteractionCreate, async (interaction) => {
if (interaction.isChatInputCommand()) {
const command = interaction.client.commands.get(
interaction.commandName,
);
// Start an Express server for webhooks so other applications can communicate with the bot.
const expressApp = express();
expressApp.use(express.json());
expressApp.use(sharedSecretMiddleware);

if (!command) {
console.error(
`No command matching ${interaction.commandName} was found.`,
);
return;
}
//Load receivers for bot's webhooks
loadReceivers(expressApp, client);

try {
await command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: "There was an error while executing this command!",
ephemeral: true,
});
} else {
await interaction.reply({
content: "There was an error while executing this command!",
ephemeral: true,
});
}
}
} else if (interaction.isButton()) {
if (interaction.customId === "verify") {
console.log("Button Pressed");
const user = interaction.member?.user;
if (!user) {
interaction.reply({
content: "There was an error while executing this command!",
ephemeral: true,
});
return;
}
const vCode = nanoid(20);
console.log(interaction.guildId);
const verification = await db
.insert(discordVerification)
.values({
code: vCode,
discordName: user.username,
discordProfilePhoto: user.avatar || "",
discordUserID: user.id as string,
discordUserTag: user.discriminator as string,
status: "pending",
guild: interaction.guildId as string,
})
.returning();

interaction.reply({
content: `Please click [this link](${c.siteUrl}/discord-verify?code=${vCode}) to verify your registration!`,
ephemeral: true,
});
}
}
const RECEIVERS_PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000;
expressApp.listen(RECEIVERS_PORT, () => {
console.log(`Bot receivers listening on port ${RECEIVERS_PORT}`);
});

client.login(process.env.DISCORD_SECRET_TOKEN);

/* WEB SERVER */

const app = new Hono();
app.get("/postMsgToServer", (h) => {
const internalAuthKey = h.req.query("access");
const serverType: string | undefined | "dev" | "prod" = h.req.query("env");
if (!internalAuthKey || internalAuthKey != process.env.INTERNAL_AUTH_KEY) {
return h.text("access denied");
}
if (!serverType || (serverType !== "dev" && serverType !== "prod")) {
return h.text("invalid env");
}

const verifyBtn = new ButtonBuilder()
.setCustomId("verify")
.setLabel("Verify")
.setStyle(ButtonStyle.Primary);

const verifyEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle("Verification")
.setURL(c.siteUrl)
.setAuthor({
name: c.botName,
iconURL: c.siteUrl + c.icon.md,
url: c.siteUrl,
})
.setDescription(
`**Verify your registration for ${c.hackathonName} ${c.itteration} to gain access to the rest of the server!**\n\nClick the "verify" button below to begin the verification process.\n\u200B`,
)
.setThumbnail(`${c.siteUrl}${c.icon.md}`)
.setFooter({
text: "Questions or issues? Contact an organizer :)",
iconURL: "https://static.acmutsa.org/Info_Simple.svg.png",
});

const channel = client.channels.cache.get(
serverType === "dev"
? (process.env.DISCORD_DEV_VERIFY_CHANNEL_ID as string)
: (process.env.DISCORD_PROD_VERIFY_CHANNEL_ID as string),
);

if (!channel || !channel.isTextBased()) {
return h.text("Invalid channel");
}

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(verifyBtn);

channel.send({
embeds: [verifyEmbed],
components: [row],
});
return h.text(`Posted to channel!`);
});

app.get("/health", (h) => {
return h.text("ok");
});

app.post("/api/checkDiscordVerification", async (h) => {
const body = await h.req.json();
const internalAuthKey = h.req.query("access");
if (!internalAuthKey || internalAuthKey != process.env.INTERNAL_AUTH_KEY) {
console.log("denied access");
return h.text("access denied");
}

if (body.code === undefined || typeof body.code !== "string") {
console.log("failed cause of malformed body");
return h.json({ success: false });
}

console.log("got here 1");
const verification = await db.query.discordVerification.findFirst({
where: eq(discordVerification.code, body.code),
});
console.log("got here 1 with verification ", verification);

if (!verification || !verification.clerkID) {
console.log("failed cause of no verification or missing clerkID");
return h.json({ success: false });
}
console.log("got here 2");

const user = await getHacker(verification.clerkID);
console.log("got here 2 with user", user);
if (!user) {
console.log("failed cause of no user in db");
return h.json({ success: false });
}

const { discordRole: userGroupRoleName } = (
c.groups as Record<string, { discordRole: string }>
)[Object.keys(c.groups)[user.hackerData.group]];

const guild = client.guilds.cache.get(verification.guild);
if (!guild) {
console.log("failed cause of no guild on intereaction");
return h.json({ success: false });
}

const role = guild.roles.cache.find(
(role) => role.name === c.botParticipantRole,
);
const userGroupRole = guild.roles.cache.find(
(role) => role.name === userGroupRoleName,
);

if (!role || !userGroupRole) {
console.log(
"failed cause could not find a role, was looking for group " +
user.hackerData.group +
" called " +
userGroupRoleName,
);
return h.json({ success: false });
}

const member = guild.members.cache.get(verification.discordUserID);

if (!member) {
console.log("failed cause could not find member");
return h.json({ success: false });
}

await member.roles.add(role);
await member.roles.add(userGroupRole);
await member.setNickname(user.firstName + " " + user.lastName);

return h.json({ success: true });
});

serve({
fetch: app.fetch,
port: process.env.PORT ? parseInt(process.env.PORT) : 4000,
});
86 changes: 86 additions & 0 deletions apps/bot/commands/postVerify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
PermissionsBitField,
} from "discord.js";
import c from "config";

export const data = new SlashCommandBuilder()
.setName("post-verify")
.setDescription("Post the verification embed to this channel (admin only)");

export const execute = async (interaction: ChatInputCommandInteraction) => {
// Ensure command is used in a guild
if (!interaction.inGuild() || !interaction.guild) {
await interaction.reply({
content: "This command can only be used in a server.",
ephemeral: true,
});
return;
}

// Check for administrator permission
const hasAdmin = interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
);
if (!hasAdmin) {
await interaction.reply({
content:
"You must have Administrator permissions to run this command.",
ephemeral: true,
});
return;
}

const channel = interaction.channel;
if (!channel || !channel.isTextBased()) {
await interaction.reply({
content: "This channel cannot receive messages from the bot.",
ephemeral: true,
});
return;
}

const verifyBtn = new ButtonBuilder()
.setCustomId("verify")
.setLabel("Verify")
.setStyle(ButtonStyle.Primary);

const verifyEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle("Verification")
.setURL(c.siteUrl)
.setAuthor({
name: c.botName,
iconURL: c.siteUrl + c.icon.md,
url: c.siteUrl,
})
.setDescription(
`**Verify your registration for ${c.hackathonName} ${c.itteration} to gain access to the rest of the server!**\n\nClick the "verify" button below to begin the verification process.\n\u200B`,
)
.setThumbnail(`${c.siteUrl}${c.icon.md}`)
.setFooter({
text: "Questions or issues? Contact an organizer :)",
iconURL: "https://static.acmutsa.org/Info_Simple.svg.png",
});

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(verifyBtn);

try {
await channel.send({ embeds: [verifyEmbed], components: [row] });
await interaction.reply({
content: "Posted verification message.",
ephemeral: true,
});
} catch (err) {
console.error("Failed to post verification message:", err);
await interaction.reply({
content: "Failed to post verification message.",
ephemeral: true,
});
}
};
Loading