Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.
Merged
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@prisma/client": "^4.13.0",
"@sapphire/discord-utilities": "^3.0.2",
"@snowcrystals/icicle": "^1.0.4",
"@snowcrystals/iglo": "^1.2.2",
"@snowcrystals/iglo": "next",
"colorette": "2.0.20",
"discord.js": "^14.9.0",
"dotenv": "^16.0.3",
Expand Down
43 changes: 42 additions & 1 deletion src/database/structures/Guild.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { GuildConfig } from "#database/types.js";
import type GitCordClient from "#discord/lib/GitCordClient.js";
import { Collection, Guild } from "discord.js";
import { ChannelType, Collection, ForumChannel, Guild, TextChannel } from "discord.js";
import GitCordGuildWebhook from "./GuildWebhook.js";
import { randomBytes } from "node:crypto";

export default class GitCordGuild {
public guild!: Guild;
Expand All @@ -28,4 +29,44 @@ export default class GitCordGuild {

return true;
}

/**
* Creates a new webhook for GitHub notifications
* @param channel The channel the notifications should be posted in
* @throws Error with reason why the creation failed
*/
public async create(channel: ForumChannel | TextChannel) {
const type = channel.type === ChannelType.GuildForum ? "FORUM" : "CHANNEL";
const webhook = await channel.createWebhook({ name: "GitCord", avatar: "https://cdn.ijskoud.dev/files/2zVGPBN3ZmId.webp" }).catch(() => {
throw new Error("Unable to create a webhook, probably missing permissions.");
});

const secret = randomBytes(64).toString("hex");

const prismaData = await this.client.prisma.guildWebhook.create({
data: { type, webhookUrl: webhook.url, guildId: this.guildId, webhookId: webhook.channelId, webhookSecret: secret }
});

const gitcordWebhook = new GitCordGuildWebhook(this, prismaData);
this.webhooks.set(webhook.channelId, gitcordWebhook);

return gitcordWebhook;
}

/**
* Deletes an existing webhook
* @param webhookId The webhook to delete
* @returns Boolean depending on the existence of the webhook
* @throws DiscordApiError if the webhook deletion fails
*/
public async delete(webhookId: string) {
const webhook = this.webhooks.get(webhookId);
if (!webhook) return false;

await webhook.discordWebhook.delete();
await this.client.prisma.guildWebhook.delete({ where: { webhookId } });

this.webhooks.delete(webhookId);
return true;
}
}
34 changes: 34 additions & 0 deletions src/discord/bot/commands/webhooks/CreateWebhookCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApplyOptions, Command, type CommandOptions } from "@snowcrystals/iglo";
import { type CommandInteraction, ApplicationCommandOptionType, ChannelType, type TextChannel, type ForumChannel } from "discord.js";
import type GitCordClient from "#discord/lib/GitCordClient.js";

@ApplyOptions<CommandOptions>({
name: "create",
description: "Creates a new webhook",
permissions: { dm: false, default: ["ManageGuild"] },
options: [
{
name: "channel",
description: "The channel to post the messages in",
type: ApplicationCommandOptionType.Channel,
channelTypes: [ChannelType.GuildText, ChannelType.GuildForum],
required: true
}
]
})
export default class extends Command<GitCordClient> {
public override async run(interaction: CommandInteraction<"cached">) {
const config = this.client.databaseManager.configs.get(interaction.guildId);
if (!config || config.webhooks.size >= 3) {
await interaction.reply("You reached the webhook limit for this guild (3/3 used)");
return;
}

await interaction.deferReply({ ephemeral: true });
const channel = interaction.options.get("channel", true);
if (!channel.channel || ![ChannelType.GuildText, ChannelType.GuildForum].includes(channel.channel.type)) return; // Fixes intellisense

const webhook = await config.create(channel.channel as TextChannel | ForumChannel);
await interaction.followUp({ content: `Webhook created, url: \`${webhook}\` --- Secret: \`${webhook.secret}\``, ephemeral: true });
}
}
56 changes: 56 additions & 0 deletions src/discord/bot/commands/webhooks/DeleteWebhookCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ApplyOptions, Command, type CommandOptions } from "@snowcrystals/iglo";
import { type CommandInteraction, ApplicationCommandOptionType, AutocompleteInteraction } from "discord.js";
import type GitCordClient from "#discord/lib/GitCordClient.js";

@ApplyOptions<CommandOptions>({
name: "delete",
description: "Deletes a webhook",
permissions: { dm: false, default: ["ManageGuild"] },
options: [
{
name: "webhook",
description: "The channel the webhook is connected to",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required: true
}
]
})
export default class extends Command<GitCordClient> {
public override async autocomplete(interaction: AutocompleteInteraction<"cached">) {
const input = interaction.options.get("webhook", true);
const config = this.client.databaseManager.configs.get(interaction.guildId);
if (!config) {
await interaction.respond([]);
return;
}

if (!input.value || typeof input.value !== "string") {
await interaction.respond(config.webhooks.map((webhook) => ({ name: webhook.id, value: webhook.id })));
return;
}

const inputValue = input.value as string; // The command input type is a string
const allOptions = config.webhooks.map((webhook) => webhook.id);
const options = allOptions.filter((opt) => opt.startsWith(inputValue) || opt.endsWith(inputValue) || opt.includes(inputValue));

await interaction.respond(options.map((opt) => ({ name: opt, value: opt })));
}

public override async run(interaction: CommandInteraction<"cached">) {
const config = this.client.databaseManager.configs.get(interaction.guildId);
if (!config) throw new Error(`Missing config for guild with id ${interaction.guildId}`);

await interaction.deferReply();
const webhookId = interaction.options.get("webhook", true);
if (!webhookId.value || typeof webhookId.value !== "string") return; // Fixes intellisense

const webhook = await config.delete(webhookId.value as string);
if (!webhook) {
await interaction.followUp("No webhook was found with the provided id");
return;
}

await interaction.followUp("Webhook deleted.");
}
}
53 changes: 53 additions & 0 deletions src/discord/bot/commands/webhooks/GetSecretCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ApplyOptions, Command, type CommandOptions } from "@snowcrystals/iglo";
import { type CommandInteraction, ApplicationCommandOptionType, AutocompleteInteraction } from "discord.js";
import type GitCordClient from "#discord/lib/GitCordClient.js";

@ApplyOptions<CommandOptions>({
name: "secret",
description: "Get the secret key of a webhook",
permissions: { dm: false, default: ["ManageGuild"] },
options: [
{
name: "webhook",
description: "The webhook id",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required: true
}
]
})
export default class extends Command<GitCordClient> {
public override async autocomplete(interaction: AutocompleteInteraction<"cached">) {
const input = interaction.options.get("webhook", true);
const config = this.client.databaseManager.configs.get(interaction.guildId);
if (!config) {
await interaction.respond([]);
return;
}

if (!input.value || typeof input.value !== "string") {
await interaction.respond(config.webhooks.map((webhook) => ({ name: webhook.id, value: webhook.id })));
return;
}

const inputValue = input.value as string; // The command input type is a string
const allOptions = config.webhooks.map((webhook) => webhook.id);
const options = allOptions.filter((opt) => opt.startsWith(inputValue) || opt.endsWith(inputValue) || opt.includes(inputValue));

await interaction.respond(options.map((opt) => ({ name: opt, value: opt })));
}

public override async run(interaction: CommandInteraction<"cached">) {
const config = this.client.databaseManager.configs.get(interaction.guildId);
if (!config) throw new Error(`Missing config for guild with id ${interaction.guildId}`);

const webhookId = interaction.options.get("webhook", true);
const webhook = config.webhooks.get(webhookId.value as string);
if (!webhook) {
await interaction.reply({ content: "No webhook found with the provided id", ephemeral: true });
return;
}

await interaction.reply({ content: `The secret for this webhook: \`${webhook.secret}\``, ephemeral: true });
}
}
25 changes: 25 additions & 0 deletions src/discord/bot/commands/webhooks/WebhookListCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApplyOptions, Command, type CommandOptions } from "@snowcrystals/iglo";
import { EmbedBuilder, type CommandInteraction } from "discord.js";
import type GitCordClient from "#discord/lib/GitCordClient.js";

@ApplyOptions<CommandOptions>({
name: "webhooks",
description: "Returns the list of created webhooks in this guild",
permissions: { dm: false, default: ["ManageGuild"] }
})
export default class extends Command<GitCordClient> {
public override async run(interaction: CommandInteraction<"cached">) {
const config = this.client.databaseManager.configs.get(interaction.guildId);
const embed = new EmbedBuilder()
.setTitle(`List of created webhooks`)
.setDescription(`${config!.webhooks.size} out of 3 slots used.`)
.addFields(
config!.webhooks.map((webhook) => ({
name: webhook.id,
value: [`Type: \`${webhook.type.toLowerCase()}\``, `Channel: <#${webhook.id}>`].join("\n")
}))
);

await interaction.reply({ embeds: [embed] });
}
}
5 changes: 3 additions & 2 deletions src/discord/lib/GitCordClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IgloClient } from "@snowcrystals/iglo";
import { IgloClient, LogLevel } from "@snowcrystals/iglo";
import { BOT_COMMANDS_DIR, BOT_INTERACTIONS_DIR, BOT_LISTENER_DIR } from "#shared/constants.js";
import GitHubManager from "#github/lib/GitHubManager.js";
import { PrismaClient } from "@prisma/client";
Expand All @@ -13,7 +13,8 @@ export default class GitCordClient extends IgloClient {
public constructor() {
super({
client: { intents: ["GuildWebhooks", "Guilds"], allowedMentions: { repliedUser: true, roles: [], users: [] } },
paths: { commands: BOT_COMMANDS_DIR, events: BOT_LISTENER_DIR, interactions: BOT_INTERACTIONS_DIR }
paths: { commands: BOT_COMMANDS_DIR, events: BOT_LISTENER_DIR, interactions: BOT_INTERACTIONS_DIR },
logger: { level: process.env.NODE_ENV === "development" ? LogLevel.Debug : LogLevel.Info, depth: 2 }
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));

// === Discord bot directory constants ===
export const BASE_BOT_DIR = join(__dirname, "..", "bot");
export const BASE_BOT_DIR = join(__dirname, "..", "discord", "bot");
export const BOT_COMMANDS_DIR = join(BASE_BOT_DIR, "commands");
export const BOT_LISTENER_DIR = join(BASE_BOT_DIR, "listeners");
export const BOT_INTERACTIONS_DIR = join(BASE_BOT_DIR, "interactions");
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -616,14 +616,14 @@ __metadata:
languageName: node
linkType: hard

"@snowcrystals/iglo@npm:^1.2.2":
version: 1.2.2
resolution: "@snowcrystals/iglo@npm:1.2.2"
"@snowcrystals/iglo@npm:next":
version: 1.3.0-next.676fbee.0
resolution: "@snowcrystals/iglo@npm:1.3.0-next.676fbee.0"
dependencies:
"@snowcrystals/icicle": 1.0.4
discord.js: ^14.9.0
lodash: ^4.17.21
checksum: 4d7b84d289a2a94bde75d1db63feaa0544c348b39802513987adf03462caed84658cf244fc3268e3c3513bd0cd7b4ca130264f809197ba8c4d08a179f5c7c1bb
checksum: 75fa847566388536e6ec2f54ce042f537d24ca6c1de673810ce9c3959e0a9531fbe6b769ed9d97aebd0b08553b65b26e8afc403e4a569e5614433e41839f7da7
languageName: node
linkType: hard

Expand Down Expand Up @@ -2549,7 +2549,7 @@ __metadata:
"@sapphire/prettier-config": ^1.4.5
"@sapphire/ts-config": ^4.0.0
"@snowcrystals/icicle": ^1.0.4
"@snowcrystals/iglo": ^1.2.2
"@snowcrystals/iglo": next
"@types/eventsource": ^1.1.11
"@types/express": ^4.17.17
"@types/node": ^18.16.0
Expand Down