Merge pull request #962 from joaotonaco/feat/discord-emoji-toggle

feat(notifications): discord decoration toggle
This commit is contained in:
Mauricio Siu
2024-12-24 14:13:09 -06:00
committed by GitHub
14 changed files with 4415 additions and 81 deletions

View File

@@ -64,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
.object({ .object({
type: z.literal("discord"), type: z.literal("discord"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
decoration: z.boolean().default(true),
}) })
.merge(notificationBaseSchema), .merge(notificationBaseSchema),
z z
@@ -195,6 +196,7 @@ export const AddNotification = () => {
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
webhookUrl: data.webhookUrl, webhookUrl: data.webhookUrl,
decoration: data.decoration,
name: data.name, name: data.name,
dockerCleanup: dockerCleanup, dockerCleanup: dockerCleanup,
}); });
@@ -397,23 +399,47 @@ export const AddNotification = () => {
)} )}
{type === "discord" && ( {type === "discord" && (
<FormField <>
control={form.control} <FormField
name="webhookUrl" control={form.control}
render={({ field }) => ( name="webhookUrl"
<FormItem> render={({ field }) => (
<FormLabel>Webhook URL</FormLabel> <FormItem>
<FormControl> <FormLabel>Webhook URL</FormLabel>
<Input <FormControl>
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ" <Input
{...field} placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
/> {...field}
</FormControl> />
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="decoration"
defaultValue={true}
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Decoration</FormLabel>
<FormDescription>
Decorate the notification with emojis.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</>
)} )}
{type === "email" && ( {type === "email" && (
@@ -708,6 +734,7 @@ export const AddNotification = () => {
} else if (type === "discord") { } else if (type === "discord") {
await testDiscordConnection({ await testDiscordConnection({
webhookUrl: form.getValues("webhookUrl"), webhookUrl: form.getValues("webhookUrl"),
decoration: form.getValues("decoration"),
}); });
} else if (type === "email") { } else if (type === "email") {
await testEmailConnection({ await testEmailConnection({

View File

@@ -28,7 +28,7 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Mail, Pen } from "lucide-react"; import { Mail, Pen } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FieldErrors, useFieldArray, useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
type NotificationSchema, type NotificationSchema,
@@ -113,6 +113,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
databaseBackup: data.databaseBackup, databaseBackup: data.databaseBackup,
type: data.notificationType, type: data.notificationType,
webhookUrl: data.discord?.webhookUrl, webhookUrl: data.discord?.webhookUrl,
decoration: data.discord?.decoration || undefined,
name: data.name, name: data.name,
dockerCleanup: data.dockerCleanup, dockerCleanup: data.dockerCleanup,
}); });
@@ -178,6 +179,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
dokployRestart: dokployRestart, dokployRestart: dokployRestart,
databaseBackup: databaseBackup, databaseBackup: databaseBackup,
webhookUrl: formData.webhookUrl, webhookUrl: formData.webhookUrl,
decoration: formData.decoration,
name: formData.name, name: formData.name,
notificationId: notificationId, notificationId: notificationId,
discordId: data?.discordId, discordId: data?.discordId,
@@ -360,23 +362,46 @@ export const UpdateNotification = ({ notificationId }: Props) => {
)} )}
{type === "discord" && ( {type === "discord" && (
<FormField <>
control={form.control} <FormField
name="webhookUrl" control={form.control}
render={({ field }) => ( name="webhookUrl"
<FormItem> render={({ field }) => (
<FormLabel>Webhook URL</FormLabel> <FormItem>
<FormControl> <FormLabel>Webhook URL</FormLabel>
<Input <FormControl>
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ" <Input
{...field} placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
/> {...field}
</FormControl> />
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="decoration"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Decoration</FormLabel>
<FormDescription>
Decorate the notification with emojis.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</>
)} )}
{type === "email" && ( {type === "email" && (
<> <>
@@ -671,6 +696,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
} else if (type === "discord") { } else if (type === "discord") {
await testDiscordConnection({ await testDiscordConnection({
webhookUrl: form.getValues("webhookUrl"), webhookUrl: form.getValues("webhookUrl"),
decoration: form.getValues("decoration"),
}); });
} else if (type === "email") { } else if (type === "email") {
await testEmailConnection({ await testEmailConnection({

View File

@@ -0,0 +1 @@
ALTER TABLE "discord" ADD COLUMN "decoration" boolean;

File diff suppressed because it is too large Load Diff

View File

@@ -365,6 +365,13 @@
"when": 1734241482851, "when": 1734241482851,
"tag": "0051_hard_gorgon", "tag": "0051_hard_gorgon",
"breakpoints": true "breakpoints": true
},
{
"idx": 52,
"version": "6",
"when": 1734809337308,
"tag": "0052_bumpy_luckman",
"breakpoints": true
} }
] ]
} }

View File

@@ -363,7 +363,7 @@ export const authRouter = createTRPCRouter({
<a href="${WEBSITE_URL}/reset-password?token=${token}"> <a href="${WEBSITE_URL}/reset-password?token=${token}">
Reset Password Reset Password
</a> </a>
`, `,
); );
}), }),
@@ -505,7 +505,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
}, },
{ {
title: " New User Registered", title: "New User Registered",
color: 0x00ff00, color: 0x00ff00,
fields: [ fields: [
{ {

View File

@@ -187,11 +187,15 @@ export const notificationRouter = createTRPCRouter({
.input(apiTestDiscordConnection) .input(apiTestDiscordConnection)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const decorate = (decoration: string, text: string) =>
`${input.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(input, { await sendDiscordNotification(input, {
title: "> `🤚` - Test Notification", title: decorate(">", "`🤚` - Test Notification"),
description: "> Hi, From Dokploy 👋", description: decorate(">", "Hi, From Dokploy 👋"),
color: 0xf3f7f4, color: 0xf3f7f4,
}); });
return true; return true;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({

View File

@@ -68,6 +68,7 @@ export const discord = pgTable("discord", {
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(), webhookUrl: text("webhookUrl").notNull(),
decoration: boolean("decoration"),
}); });
export const email = pgTable("email", { export const email = pgTable("email", {
@@ -171,6 +172,7 @@ export const apiCreateDiscord = notificationsSchema
}) })
.extend({ .extend({
webhookUrl: z.string().min(1), webhookUrl: z.string().min(1),
decoration: z.boolean(),
}) })
.required(); .required();
@@ -180,9 +182,13 @@ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
adminId: z.string().optional(), adminId: z.string().optional(),
}); });
export const apiTestDiscordConnection = apiCreateDiscord.pick({ export const apiTestDiscordConnection = apiCreateDiscord
webhookUrl: true, .pick({
}); webhookUrl: true,
})
.extend({
decoration: z.boolean().optional(),
});
export const apiCreateEmail = notificationsSchema export const apiCreateEmail = notificationsSchema
.pick({ .pick({

View File

@@ -204,6 +204,7 @@ export const createDiscordNotification = async (
.insert(discord) .insert(discord)
.values({ .values({
webhookUrl: input.webhookUrl, webhookUrl: input.webhookUrl,
decoration: input.decoration,
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -272,6 +273,7 @@ export const updateDiscordNotification = async (
.update(discord) .update(discord)
.set({ .set({
webhookUrl: input.webhookUrl, webhookUrl: input.webhookUrl,
decoration: input.decoration,
}) })
.where(eq(discord.discordId, input.discordId)) .where(eq(discord.discordId, input.discordId))
.returning() .returning()

View File

@@ -59,46 +59,49 @@ export const sendBuildErrorNotifications = async ({
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: "> `⚠️` Build Failed", title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245, color: 0xed4245,
fields: [ fields: [
{ {
name: "`🛠️` Project", name: decorate("`🛠️`", "Project"),
value: projectName, value: projectName,
inline: true, inline: true,
}, },
{ {
name: "`⚙️` Application", name: decorate("`⚙️`", "Application"),
value: applicationName, value: applicationName,
inline: true, inline: true,
}, },
{ {
name: "`❔` Type", name: decorate("`❔`", "Type"),
value: applicationType, value: applicationType,
inline: true, inline: true,
}, },
{ {
name: "`📅` Date", name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: "`⌚` Time", name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: "`❓`Type", name: decorate("`❓`", "Type"),
value: "Failed", value: "Failed",
inline: true, inline: true,
}, },
{ {
name: "`⚠️` Error Message", name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${errorMessage}\`\`\``,
}, },
{ {
name: "`🧷` Build Link", name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`, value: `[Click here to access build link](${buildLink})`,
}, },
], ],
@@ -114,15 +117,15 @@ export const sendBuildErrorNotifications = async ({
telegram, telegram,
` `
<b>⚠️ Build Failed</b> <b>⚠️ Build Failed</b>
<b>Project:</b> ${projectName} <b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName} <b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType} <b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()} <b>Time:</b> ${date.toLocaleString()}
<b>Error:</b> <b>Error:</b>
<pre>${errorMessage}</pre> <pre>${errorMessage}</pre>
<b>Build Details:</b> ${buildLink} <b>Build Details:</b> ${buildLink}
`, `,
); );

View File

@@ -57,42 +57,45 @@ export const sendBuildSuccessNotifications = async ({
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: "> `✅` Build Success", title: "> `✅` Build Success",
color: 0x57f287, color: 0x57f287,
fields: [ fields: [
{ {
name: "`🛠️` Project", name: decorate("`🛠️`", "Project"),
value: projectName, value: projectName,
inline: true, inline: true,
}, },
{ {
name: "`⚙️` Application", name: decorate("`⚙️`", "Application"),
value: applicationName, value: applicationName,
inline: true, inline: true,
}, },
{ {
name: "`❔` Application Type", name: decorate("`❔`", "Type"),
value: applicationType, value: applicationType,
inline: true, inline: true,
}, },
{ {
name: "`📅` Date", name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: "`⌚` Time", name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: "`❓` Type", name: decorate("`❓`", "Type"),
value: "Successful", value: "Successful",
inline: true, inline: true,
}, },
{ {
name: "`🧷` Build Link", name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`, value: `[Click here to access build link](${buildLink})`,
}, },
], ],
@@ -108,12 +111,12 @@ export const sendBuildSuccessNotifications = async ({
telegram, telegram,
` `
<b>✅ Build Success</b> <b>✅ Build Success</b>
<b>Project:</b> ${projectName} <b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName} <b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType} <b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()} <b>Time:</b> ${date.toLocaleString()}
<b>Build Details:</b> ${buildLink} <b>Build Details:</b> ${buildLink}
`, `,
); );

View File

@@ -62,40 +62,43 @@ export const sendDatabaseBackupNotifications = async ({
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: title:
type === "success" type === "success"
? "> `✅` Database Backup Successful" ? decorate(">", "`✅` Database Backup Successful")
: "> `❌` Database Backup Failed", : decorate(">", "`❌` Database Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245, color: type === "success" ? 0x57f287 : 0xed4245,
fields: [ fields: [
{ {
name: "`🛠️` Project", name: decorate("`🛠️`", "Project"),
value: projectName, value: projectName,
inline: true, inline: true,
}, },
{ {
name: "`⚙️` Application", name: decorate("`⚙️`", "Application"),
value: applicationName, value: applicationName,
inline: true, inline: true,
}, },
{ {
name: "`❔` Database", name: decorate("`❔`", "Database"),
value: databaseType, value: databaseType,
inline: true, inline: true,
}, },
{ {
name: "`📅` Date", name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: "`⌚` Time", name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: "`❓` Type", name: decorate("`❓`", "Type"),
value: type value: type
.replace("error", "Failed") .replace("error", "Failed")
.replace("success", "Successful"), .replace("success", "Successful"),
@@ -104,7 +107,7 @@ export const sendDatabaseBackupNotifications = async ({
...(type === "error" && errorMessage ...(type === "error" && errorMessage
? [ ? [
{ {
name: "`⚠️` Error Message", name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${errorMessage}\`\`\``,
}, },
] ]
@@ -121,12 +124,12 @@ export const sendDatabaseBackupNotifications = async ({
const statusEmoji = type === "success" ? "✅" : "❌"; const statusEmoji = type === "success" ? "✅" : "❌";
const messageText = ` const messageText = `
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b> <b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
<b>Project:</b> ${projectName} <b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName} <b>Application:</b> ${applicationName}
<b>Type:</b> ${databaseType} <b>Type:</b> ${databaseType}
<b>Time:</b> ${date.toLocaleString()} <b>Time:</b> ${date.toLocaleString()}
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"} <b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""} ${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
`; `;

View File

@@ -45,27 +45,30 @@ export const sendDockerCleanupNotifications = async (
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: "> `✅` Docker Cleanup", title: decorate(">", "`✅` Docker Cleanup"),
color: 0x57f287, color: 0x57f287,
fields: [ fields: [
{ {
name: "`📅` Date", name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: "`⌚` Time", name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: "`❓` Type", name: decorate("`❓`", "Type"),
value: "Successful", value: "Successful",
inline: true, inline: true,
}, },
{ {
name: "`📜` Message", name: decorate("`📜`", "Message"),
value: `\`\`\`${message}\`\`\``, value: `\`\`\`${message}\`\`\``,
}, },
], ],

View File

@@ -34,22 +34,25 @@ export const sendDokployRestartNotifications = async () => {
} }
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: "> `✅` Dokploy Server Restarted", title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287, color: 0x57f287,
fields: [ fields: [
{ {
name: "`📅` Date", name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`, value: `<t:${unixDate}:D>`,
inline: true, inline: true,
}, },
{ {
name: "`⌚` Time", name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`, value: `<t:${unixDate}:t>`,
inline: true, inline: true,
}, },
{ {
name: "`❓` Type", name: decorate("`❓`", "Type"),
value: "Successful", value: "Successful",
inline: true, inline: true,
}, },