From 70eb178270418969473a272bcbe6b4b8f102e7e5 Mon Sep 17 00:00:00 2001 From: c43721 Date: Sat, 16 Nov 2024 20:13:47 -0600 Subject: [PATCH] feat(webhook): add support for webhooks in guilds --- src/commands/Admin/webhooks.ts | 173 ++++++++++++++++++ ...{createWebhook.ts => createUserWebhook.ts} | 0 ...{deleteWebhook.ts => deleteUserWebhook.ts} | 0 .../en-US/commands/guild-webhook.json | 17 ++ src/lib/api/utils/webhook-helper.ts | 15 +- .../i18n/all/keys/commands/guild-webhook.ts | 15 ++ src/lib/i18n/all/keys/commands/index.ts | 1 + src/lib/i18n/all/keys/commands/webhook.ts | 2 +- 8 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 src/commands/Admin/webhooks.ts rename src/interaction-handlers/{createWebhook.ts => createUserWebhook.ts} (100%) rename src/interaction-handlers/{deleteWebhook.ts => deleteUserWebhook.ts} (100%) create mode 100644 src/languages/en-US/commands/guild-webhook.json create mode 100644 src/lib/i18n/all/keys/commands/guild-webhook.ts diff --git a/src/commands/Admin/webhooks.ts b/src/commands/Admin/webhooks.ts new file mode 100644 index 00000000..e1feb1b0 --- /dev/null +++ b/src/commands/Admin/webhooks.ts @@ -0,0 +1,173 @@ +import { type CommandOptions, Command } from "@sapphire/framework"; +import { ApplyOptions } from "@sapphire/decorators"; +import { EmbedBuilder, codeBlock, InteractionContextType, ChannelType, PermissionFlagsBits } from "discord.js"; +import { isNullOrUndefinedOrEmpty } from "@sapphire/utilities"; +import { PayloadCommand } from "#lib/structs/commands/PayloadCommand"; +import { LanguageKeys } from "#lib/i18n/all"; +import PayloadColors from "#utils/colors"; +import { guild, webhook } from "#root/drizzle/schema"; +import { eq } from "drizzle-orm"; +import { fetchT, getLocalizedData } from "@sapphire/plugin-i18next"; +import { generate } from "generate-password"; +import { sendTest } from "#lib/api/utils/webhook-helper"; + +@ApplyOptions({ + description: LanguageKeys.Commands.Webhook.Description, + detailedDescription: LanguageKeys.Commands.Webhook.DetailedDescription, +}) +export class UserCommand extends PayloadCommand { + async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + const t = await fetchT(interaction); + const { client } = this.container; + + const [guildWebhook] = await this.database + .select({ webhookId: guild.webhookId }) + .from(guild) + .where(eq(guild.id, interaction.guildId)); + + switch (interaction.commandName) { + case "add": { + const channel = interaction.options.getChannel("channel"); + + const didSucceed = await sendTest(this.container.client, "channels", channel?.id); + + if (!didSucceed) { + const embed = new EmbedBuilder({ + author: { + name: client.user.username, + iconURL: client.user.displayAvatarURL(), + }, + title: t(LanguageKeys.Commands.Webhook.EmbedTitle), + description: t(LanguageKeys.Commands.GuildWebhook.AddFailed), + color: PayloadColors.Payload, + }); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + + return; + } + + const secret = generate({ numbers: true, length: 24 }); + + const [createdWebhook] = await this.database + .insert(webhook) + .values({ + id: interaction.guildId, + type: "channels", + value: secret, + }) + .returning(); + + await this.database + .insert(guild) + .values({ + id: channel.id, + webhookId: createdWebhook.id, + }) + .onConflictDoUpdate({ + set: { + webhookId: createdWebhook.id, + }, + target: guild.id, + }); + + const embed = new EmbedBuilder({ + title: t(LanguageKeys.Commands.Webhook.EmbedTitle), + description: codeBlock(createdWebhook.value), + color: PayloadColors.Payload, + }); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + + return; + } + case "delete": { + await this.container.database.delete(webhook).where(eq(webhook.id, interaction.user.id)); + + await interaction.reply({ + content: t(LanguageKeys.Commands.Webhook.DeletedWebhook), + ephemeral: true, + }); + + return; + } + case "show": { + const embed = new EmbedBuilder({ + title: t(LanguageKeys.Commands.Webhook.EmbedTitle), + description: t(LanguageKeys.Commands.Webhook.NoWebhook), + color: PayloadColors.Payload, + }); + + if (!isNullOrUndefinedOrEmpty(guildWebhook)) { + const [wbhk] = await this.database + .select({ value: webhook.value }) + .from(webhook) + .where(eq(webhook.id, guildWebhook.webhookId)); + + embed.setDescription(codeBlock(wbhk.value)); + } + + await interaction.reply({ embeds: [embed], ephemeral: true }); + + return; + } + } + } + + public override registerApplicationCommands(registry: Command.Registry) { + const rootNameLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.Name); + const rootDescriptionLocalizations = getLocalizedData(this.description); + + const addNameLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.AddName); + const addDescriptionLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.AddDescription); + const channelIdNameLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.AddChannelIdName); + const channelIdDescriptionLocalizations = getLocalizedData( + LanguageKeys.Commands.GuildWebhook.AddChannelIdDescription, + ); + + const removeNameLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.RemoveName); + const removeDescriptionLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.RemoveDescription); + + const showNameLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.ShowName); + const showDescriptionLocalizations = getLocalizedData(LanguageKeys.Commands.GuildWebhook.ShowDescription); + + registry.registerChatInputCommand(builder => + builder + .setName(this.name) + .setDescription(rootDescriptionLocalizations.localizations["en-US"]) + .setContexts(InteractionContextType.Guild) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDescriptionLocalizations(rootDescriptionLocalizations.localizations) + .setNameLocalizations(rootNameLocalizations.localizations) + .addSubcommand(sub => + sub + .setName("add") + .setDescription(addDescriptionLocalizations.localizations["en-US"]) + .setNameLocalizations(addNameLocalizations.localizations) + .setDescriptionLocalizations(addDescriptionLocalizations.localizations) + .addChannelOption(input => + input + .setName("channel") + .setDescription(channelIdDescriptionLocalizations.localizations["en-US"]) + .setNameLocalizations(channelIdNameLocalizations.localizations) + .setDescriptionLocalizations(channelIdDescriptionLocalizations.localizations) + .addChannelTypes(ChannelType.GuildText), + ), + ) + .addSubcommand(sub => + sub + .setName("delete") + .setDescription(removeDescriptionLocalizations.localizations["en-US"]) + .setNameLocalizations(removeNameLocalizations.localizations) + .setDescriptionLocalizations(removeDescriptionLocalizations.localizations), + ) + .addSubcommand(sub => + sub + .setName("show") + .setDescription(showDescriptionLocalizations.localizations["en-US"]) + .setNameLocalizations(showNameLocalizations.localizations) + .setDescriptionLocalizations(showDescriptionLocalizations.localizations), + ), + ); + } +} diff --git a/src/interaction-handlers/createWebhook.ts b/src/interaction-handlers/createUserWebhook.ts similarity index 100% rename from src/interaction-handlers/createWebhook.ts rename to src/interaction-handlers/createUserWebhook.ts diff --git a/src/interaction-handlers/deleteWebhook.ts b/src/interaction-handlers/deleteUserWebhook.ts similarity index 100% rename from src/interaction-handlers/deleteWebhook.ts rename to src/interaction-handlers/deleteUserWebhook.ts diff --git a/src/languages/en-US/commands/guild-webhook.json b/src/languages/en-US/commands/guild-webhook.json new file mode 100644 index 00000000..ac8daf1e --- /dev/null +++ b/src/languages/en-US/commands/guild-webhook.json @@ -0,0 +1,17 @@ +{ + "name": "guildwebhook", + "description": "Provides a set of actions to work with the Payload webhook integration", + "detailedDescription": { + "usages": [], + "details": "See more on (Github)[https://github.com/payload-bot/payload-logs-plugin]" + }, + "addName": "add", + "addDescription": "Creates a webhook for a given channel", + "addChannelIdName": "channelid", + "addChannelIdDescription": "The channel id to send webhooks to", + "addFailed": "Failed to create the webhook", + "removeName": "remove", + "removeDescription": "Removes the webhook", + "showName": "show", + "showDescription": "Shows the webhook secret for the channel" +} \ No newline at end of file diff --git a/src/lib/api/utils/webhook-helper.ts b/src/lib/api/utils/webhook-helper.ts index b9984db6..11338ea0 100644 --- a/src/lib/api/utils/webhook-helper.ts +++ b/src/lib/api/utils/webhook-helper.ts @@ -1,6 +1,7 @@ import config from "#root/config"; import { EmbedColors } from "#utils/colors"; import { capturePage } from "#utils/screenshot"; +import { container } from "@sapphire/pieces"; import { type Client, type TextChannel, @@ -125,15 +126,19 @@ export async function sendLogPreview(client: Client, { logsId, targetId, demosId } export async function sendTest(client: Client, scope: WebhookTargetType, id: string) { + if (id == null) { + return false; + } + const target = (await client[scope].fetch(id).catch(() => null)) as TargetReturnType; if (target == null) { - throw new Error("Bad discord target id"); + return false; } const embed = new EmbedBuilder({ title: "Webhook Test", - description: "Successful webhook test!", + description: "You will receive log previews in this channel", footer: { text: "Rendered from Webhook", }, @@ -142,9 +147,11 @@ export async function sendTest(client: Client, scope: WebhookTargetType, id: str }); try { - await sendWebhook(target, embed, null, null); + const data = await sendWebhook(target, embed, null, null); + return data != null; } catch (err) { - throw err; + container.logger.error(err); + return false; } } diff --git a/src/lib/i18n/all/keys/commands/guild-webhook.ts b/src/lib/i18n/all/keys/commands/guild-webhook.ts new file mode 100644 index 00000000..f4b8dd8e --- /dev/null +++ b/src/lib/i18n/all/keys/commands/guild-webhook.ts @@ -0,0 +1,15 @@ +import { T } from "#lib/types"; + +export const Name = T("commands/guild-webhook:name"); +export const Description = T("commands/guild-webhook:description"); +export const DetailedDescription = T("commands/guild-webhook:detailedDescription"); + +export const AddName = T("commands/guild-webhook:addName"); +export const AddDescription = T("commands/guild-webhook:addDescription"); +export const AddChannelIdName = T("commands/guild-webhook:addChannelIdName"); +export const AddChannelIdDescription = T("commands/guild-webhook:addChannelIdDescription"); +export const AddFailed = T("commands/guild-webhook:addFailed"); +export const RemoveName = T("commands/guild-webhook:removeName"); +export const RemoveDescription = T("commands/guild-webhook:removeDescription"); +export const ShowName = T("commands/guild-webhook:removeName"); +export const ShowDescription = T("commands/guild-webhook:removeDescription"); \ No newline at end of file diff --git a/src/lib/i18n/all/keys/commands/index.ts b/src/lib/i18n/all/keys/commands/index.ts index e1f186e4..d7363bce 100644 --- a/src/lib/i18n/all/keys/commands/index.ts +++ b/src/lib/i18n/all/keys/commands/index.ts @@ -10,3 +10,4 @@ export * as Settings from "./settings.js"; export * as Invite from "./invite.js"; export * as Help from "./help.js"; export * as Webhook from "./webhook.js"; +export * as GuildWebhook from "./guild-webhook.js"; diff --git a/src/lib/i18n/all/keys/commands/webhook.ts b/src/lib/i18n/all/keys/commands/webhook.ts index edeaaf02..f6005182 100644 --- a/src/lib/i18n/all/keys/commands/webhook.ts +++ b/src/lib/i18n/all/keys/commands/webhook.ts @@ -1,4 +1,4 @@ -import { FT, T } from "#lib/types"; +import { T } from "#lib/types"; export const Name = T("commands/webhook:name"); export const Description = T("commands/webhook:description");