diff --git a/embedg-app/src/components/MessageExportImport.tsx b/embedg-app/src/components/MessageExportImport.tsx index 19e77cf3c..c2209b0c4 100644 --- a/embedg-app/src/components/MessageExportImport.tsx +++ b/embedg-app/src/components/MessageExportImport.tsx @@ -85,6 +85,7 @@ export default function MessageExportImport({ messages, guildId }: Props) { } ); } else { + console.log(parsed.error); createToast({ title: "Failed to import", message: `Data did not match the expected format`, diff --git a/embedg-app/src/components/MessageRestoreButton.tsx b/embedg-app/src/components/MessageRestoreButton.tsx index 51fbf84d2..a3111d23f 100644 --- a/embedg-app/src/components/MessageRestoreButton.tsx +++ b/embedg-app/src/components/MessageRestoreButton.tsx @@ -7,7 +7,7 @@ import { } from "../api/mutations"; import { webhookUrlRegex } from "../discord/util"; import { MessageRestoreResponseDataWire } from "../api/wire"; -import { parseMessageWithAction } from "../discord/schema"; +import { parseMessageWithAction } from "../discord/restoreSchema"; import { useCurrentMessageStore } from "../state/message"; import { useCurrentAttachmentsStore } from "../state/attachments"; import { getUniqueId } from "../util"; diff --git a/embedg-app/src/discord/restoreSchema.ts b/embedg-app/src/discord/restoreSchema.ts new file mode 100644 index 000000000..c87b6c58f --- /dev/null +++ b/embedg-app/src/discord/restoreSchema.ts @@ -0,0 +1,301 @@ +import { z } from "zod"; +import { getUniqueId } from "../util"; + +export const uniqueIdSchema = z.number(); + +export type UniqueId = z.infer; + +export const embedFooterTextSchema = z.optional(z.string()); + +export type EmbedFooterText = z.infer; + +export const embedFooterIconUrlSchema = z.optional(z.string()); + +export type EmbedFooterIconUrl = z.infer; + +export const embedFooterSchema = z.optional( + z.object({ + text: embedFooterTextSchema, + icon_url: embedFooterIconUrlSchema, + }) +); + +export type EmbedFooter = z.infer; + +export const embedImageUrlSchema = z.optional(z.string()); + +export type EmbedImageUrl = z.infer; + +export const embedImageSchema = z.optional( + z.object({ + url: embedImageUrlSchema, + }) +); + +export type EmbedImage = z.infer; + +export const embedThumbnailUrlSchema = z.optional(z.string()); + +export type EmbedThumbnailUrl = z.infer; + +export const embedThumbnailSchema = z.optional( + z.object({ + url: embedThumbnailUrlSchema, + }) +); + +export type EmbedThumbnail = z.infer; + +export const embedAuthorNameSchema = z.string(); + +export type EmbedAuthorName = z.infer; + +export const embedAuthorUrlSchema = z.optional(z.string()); + +export type EmbedAuthorUrl = z.infer; + +export const embedAuthorIconUrlSchema = z.optional(z.string()); + +export type EmbedAuthorIconUrl = z.infer; + +export const embedAuthorSchema = z.optional( + z.object({ + name: embedAuthorNameSchema, + url: embedAuthorUrlSchema, + icon_url: embedAuthorIconUrlSchema, + }) +); + +export type EmbedAuthor = z.infer; + +export const embedFieldNameSchema = z.string(); + +export type EmbedFieldName = z.infer; + +export const embedFieldValueSchema = z.string(); + +export type EmbedFieldValue = z.infer; + +export const embedFieldInlineSchma = z.optional(z.boolean()); + +export type EmbedFieldInline = z.infer; + +export const embedFieldSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + name: embedFieldNameSchema, + value: embedFieldValueSchema, + inline: embedFieldInlineSchma, +}); + +export type EmbedField = z.infer; + +export const embedtitleSchema = z.optional(z.string()); + +export type EmbedTitle = z.infer; + +export const embedDescriptionSchema = z.optional(z.string()); + +export type EmbedDescription = z.infer; + +export const embedUrlSchema = z.optional(z.string()); + +export type EmbedUrl = z.infer; + +export const embedTimestampSchema = z.optional(z.string()); + +export type EmbedTimestamp = z.infer; + +export const embedColor = z.optional(z.number()); + +export type EmbedColor = z.infer; + +export const embedSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + title: embedtitleSchema, + description: embedDescriptionSchema, + url: embedUrlSchema, + timestamp: embedTimestampSchema, + color: embedColor, + footer: embedFooterSchema, + author: embedAuthorSchema, + image: embedImageSchema, + thumbnail: embedThumbnailSchema, + fields: z.preprocess( + (d) => d ?? undefined, + z.array(embedFieldSchema).default([]) + ), +}); + +export type MessageEmbed = z.infer; + +export const buttonStyleSchema = z + .literal(1) + .or(z.literal(2)) + .or(z.literal(3)) + .or(z.literal(4)) + .or(z.literal(5)); + +export type MessageComponentButtonStyle = z.infer; + +export const buttonSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + type: z.literal(2), + style: buttonStyleSchema, + label: z.string(), + url: z.optional(z.string()), + action_set_id: z.preprocess( + (d) => d ?? undefined, + z.string().default(() => getUniqueId().toString()) + ), +}); + +export type MessageComponentButton = z.infer; + +export const selectMenuOptionSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + label: z.string(), + action_set_id: z.preprocess( + (d) => d ?? undefined, + z.string().default(() => getUniqueId().toString()) + ), +}); + +export type MessageComponentSelectMenuOption = z.infer< + typeof selectMenuOptionSchema +>; + +export const selectMenuSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + type: z.literal(3), + placeholder: z.optional(z.string()), + options: z.array(selectMenuOptionSchema), +}); + +export type MessageComponentSelectMenu = z.infer; + +export const actionRowSchema = z.object({ + id: z.preprocess( + (d) => d ?? undefined, + uniqueIdSchema.default(() => getUniqueId()) + ), + type: z.literal(1), + components: z.array(buttonSchema.or(selectMenuSchema)), +}); + +export type MessageComponentActionRow = z.infer; + +export const messageAction = z + .object({ + type: z.literal(1), // text response + id: uniqueIdSchema.default(() => getUniqueId()), + text: z.string(), + }) + .or( + z.object({ + type: z.literal(2).or(z.literal(3)).or(z.literal(4)), // toggle, add, remove role + id: uniqueIdSchema.default(() => getUniqueId()), + target_id: z.string(), + }) + ); + +export type MessageAction = z.infer; + +export const messageActionSet = z.object({ + actions: z.array(messageAction), +}); + +export type MessageActionSet = z.infer; + +export const messageContentSchema = z.string(); + +export type MessageContent = z.infer; + +export const webhookUsernameSchema = z.optional(z.string()); + +export type WebhookUsername = z.infer; + +export const webhookAvatarUrlSchema = z.optional(z.string()); + +export type WebhookAvatarUrl = z.infer; + +export const messageTtsSchema = z.boolean(); + +export type MessageTts = z.infer; + +export const messageAllowedMentionsSchema = z.optional( + z.object({ + parse: z.array( + z.literal("users").or(z.literal("roles")).or(z.literal("everyone")) + ), + roles: z.array(z.string()), + users: z.array(z.string()), + replied_user: z.boolean(), + }) +); + +export const messageThreadName = z.optional(z.string()); + +export const messageSchema = z.object({ + content: z.preprocess( + (d) => d ?? undefined, + messageContentSchema.default("") + ), + username: webhookUsernameSchema, + avatar_url: webhookAvatarUrlSchema, + tts: z.preprocess((d) => d ?? undefined, messageTtsSchema.default(false)), + embeds: z.preprocess((d) => d ?? undefined, z.array(embedSchema).default([])), + allowed_mentions: messageAllowedMentionsSchema, + components: z.preprocess( + (d) => d ?? undefined, + z.array(actionRowSchema).default([]) + ), + thread_name: messageThreadName, + actions: z.preprocess( + (d) => d ?? undefined, + z.record(z.string(), messageActionSet).default({}) + ), +}); + +export type Message = z.infer; + +export function parseMessageWithAction(raw: any) { + const parsedData = messageSchema.parse(raw); + + // create messing action sets + for (const row of parsedData.components) { + for (const comp of row.components) { + if (comp.type === 2) { + if (!parsedData.actions[comp.action_set_id]) { + parsedData.actions[comp.action_set_id] = { + actions: [], + }; + } + } else { + for (const option of comp.options) { + if (!parsedData.actions[option.action_set_id]) { + parsedData.actions[option.action_set_id] = { + actions: [], + }; + } + } + } + } + } + + return parsedData; +} diff --git a/embedg-app/src/discord/schema.ts b/embedg-app/src/discord/schema.ts index 0b262deeb..66c1e4202 100644 --- a/embedg-app/src/discord/schema.ts +++ b/embedg-app/src/discord/schema.ts @@ -1,4 +1,4 @@ -import { ZodObject, ZodType, z } from "zod"; +import { z } from "zod"; import { getUniqueId } from "../util"; const HOSTNAME_RE = new RegExp("\\.[a-zA-Z]{2,}$"); @@ -117,7 +117,7 @@ export const embedFieldInlineSchma = z.optional(z.boolean()); export type EmbedFieldInline = z.infer; export const embedFieldSchema = z.object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), name: embedFieldNameSchema, value: embedFieldValueSchema, inline: embedFieldInlineSchma, @@ -147,7 +147,7 @@ export type EmbedColor = z.infer; export const embedSchema = z .object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), title: embedtitleSchema, description: embedDescriptionSchema, url: embedUrlSchema, @@ -157,7 +157,7 @@ export const embedSchema = z author: embedAuthorSchema, image: embedImageSchema, thumbnail: embedThumbnailSchema, - fields: nullDefault(z.array(embedFieldSchema).default([])), + fields: z.array(embedFieldSchema).default([]), }) .superRefine((data, ctx) => { if ( @@ -197,24 +197,20 @@ export const buttonStyleSchema = z export type MessageComponentButtonStyle = z.infer; export const buttonSchema = z.object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), type: z.literal(2), style: buttonStyleSchema, label: z.string().min(1), url: z.optional(z.string().refine(...urlRefinement)), - action_set_id: nullDefault( - z.string().default(() => getUniqueId().toString()) - ), + action_set_id: z.string().default(() => getUniqueId().toString()), }); export type MessageComponentButton = z.infer; export const selectMenuOptionSchema = z.object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), label: z.string().min(1).max(100), - action_set_id: nullDefault( - z.string().default(() => getUniqueId().toString()) - ), + action_set_id: z.string().default(() => getUniqueId().toString()), }); export type MessageComponentSelectMenuOption = z.infer< @@ -222,7 +218,7 @@ export type MessageComponentSelectMenuOption = z.infer< >; export const selectMenuSchema = z.object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), type: z.literal(3), placeholder: z.optional(z.string().max(150)), options: z.array(selectMenuOptionSchema).min(1).max(25), @@ -231,7 +227,7 @@ export const selectMenuSchema = z.object({ export type MessageComponentSelectMenu = z.infer; export const actionRowSchema = z.object({ - id: nullDefault(uniqueIdSchema.default(() => getUniqueId())), + id: uniqueIdSchema.default(() => getUniqueId()), type: z.literal(1), components: z.array(buttonSchema.or(selectMenuSchema)).min(1).max(5), }); @@ -293,15 +289,15 @@ export const messageThreadName = z.optional(z.string().max(100)); export const messageSchema = z .object({ - content: nullDefault(messageContentSchema.default("")), + content: messageContentSchema.default(""), username: webhookUsernameSchema, avatar_url: webhookAvatarUrlSchema, - tts: nullDefault(messageTtsSchema.default(false)), - embeds: nullDefault(z.array(embedSchema).max(10).default([])), + tts: messageTtsSchema.default(false), + embeds: z.array(embedSchema).max(10).default([]), allowed_mentions: messageAllowedMentionsSchema, - components: nullDefault(z.array(actionRowSchema).max(5).default([])), + components: z.array(actionRowSchema).max(5).default([]), thread_name: messageThreadName, - actions: nullDefault(z.record(z.string(), messageActionSet).default({})), + actions: z.record(z.string(), messageActionSet).default({}), }) .superRefine((data, ctx) => { // this currently doesn't take attachments into account @@ -315,34 +311,3 @@ export const messageSchema = z }); export type Message = z.infer; - -export function parseMessageWithAction(raw: any) { - const parsedData = messageSchema.parse(raw); - - // create messing action sets - for (const row of parsedData.components) { - for (const comp of row.components) { - if (comp.type === 2) { - if (!parsedData.actions[comp.action_set_id]) { - parsedData.actions[comp.action_set_id] = { - actions: [], - }; - } - } else { - for (const option of comp.options) { - if (!parsedData.actions[option.action_set_id]) { - parsedData.actions[option.action_set_id] = { - actions: [], - }; - } - } - } - } - } - - return parsedData; -} - -function nullDefault(schema: ZodType) { - return z.preprocess((d) => d ?? undefined, schema); -} diff --git a/embedg-app/src/views/editor/magic.tsx b/embedg-app/src/views/editor/magic.tsx index 8a1f7d93b..f65cd75af 100644 --- a/embedg-app/src/views/editor/magic.tsx +++ b/embedg-app/src/views/editor/magic.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import { useGenerateMagicMessageMutation } from "../../api/mutations"; import EditorModal from "../../components/EditorModal"; import MessagePreview from "../../components/MessagePreview"; -import { messageSchema, parseMessageWithAction } from "../../discord/schema"; +import { parseMessageWithAction } from "../../discord/restoreSchema"; import { useCurrentMessageStore } from "../../state/message"; export default function MagicView() { diff --git a/embedg-app/src/views/editor/messages.tsx b/embedg-app/src/views/editor/messages.tsx index 8fde02974..aa054b39b 100644 --- a/embedg-app/src/views/editor/messages.tsx +++ b/embedg-app/src/views/editor/messages.tsx @@ -22,6 +22,7 @@ import { messageSchema } from "../../discord/schema"; import { useNavigate } from "react-router-dom"; import MessageExportImport from "../../components/MessageExportImport"; import { useToasts } from "../../util/toasts"; +import { parseMessageWithAction } from "../../discord/restoreSchema"; function formatUpdatedAt(updatedAt: string): string { return parseISO(updatedAt).toLocaleString(); @@ -105,7 +106,7 @@ export default function MessagesView() { function restoreMessage(message: SavedMessageWire) { try { - const data = messageSchema.parse(message.data); + const data = parseMessageWithAction(message.data); useCurrentMessageStore.setState(data); navigate("/app"); } catch (e) {