From 55ebc8c6eecf66a32d4b29b07693d54dc80a7a47 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 10:24:34 -0500 Subject: [PATCH 1/7] Add notificationSchema --- app/soapbox/schemas/chat-message.ts | 10 +++ app/soapbox/schemas/emoji-reaction.ts | 3 +- app/soapbox/schemas/notification.ts | 104 ++++++++++++++++++++++++++ app/soapbox/schemas/utils.ts | 5 +- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/schemas/chat-message.ts create mode 100644 app/soapbox/schemas/notification.ts diff --git a/app/soapbox/schemas/chat-message.ts b/app/soapbox/schemas/chat-message.ts new file mode 100644 index 000000000..a64ffec0b --- /dev/null +++ b/app/soapbox/schemas/chat-message.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +import { normalizeChatMessage } from 'soapbox/normalizers'; +import { toSchema } from 'soapbox/utils/normalizers'; + +const chatMessageSchema = toSchema(normalizeChatMessage); + +type ChatMessage = z.infer; + +export { chatMessageSchema, type ChatMessage }; \ No newline at end of file diff --git a/app/soapbox/schemas/emoji-reaction.ts b/app/soapbox/schemas/emoji-reaction.ts index 55c1762a0..28271fe29 100644 --- a/app/soapbox/schemas/emoji-reaction.ts +++ b/app/soapbox/schemas/emoji-reaction.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; -/** Validates the string as an emoji. */ -const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)); +import { emojiSchema } from './utils'; /** Pleroma emoji reaction. */ const emojiReactionSchema = z.object({ diff --git a/app/soapbox/schemas/notification.ts b/app/soapbox/schemas/notification.ts new file mode 100644 index 000000000..3c77de6bf --- /dev/null +++ b/app/soapbox/schemas/notification.ts @@ -0,0 +1,104 @@ +import { z } from 'zod'; + +import { accountSchema } from './account'; +import { chatMessageSchema } from './chat-message'; +import { statusSchema } from './status'; +import { emojiSchema } from './utils'; + +const baseNotificationSchema = z.object({ + account: accountSchema, + created_at: z.string().datetime().catch(new Date().toUTCString()), + id: z.string(), + type: z.string(), + total_count: z.number().optional().catch(undefined), // TruthSocial +}); + +const mentionNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('mention'), + status: statusSchema, +}); + +const statusNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('status'), + status: statusSchema, +}); + +const reblogNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('reblog'), + status: statusSchema, +}); + +const followNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('follow'), +}); + +const followRequestNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('follow_request'), +}); + +const favouriteNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('favourite'), + status: statusSchema, +}); + +const pollNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('poll'), + status: statusSchema, +}); + +const updateNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('update'), + status: statusSchema, +}); + +const moveNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('move'), + target: accountSchema, +}); + +const chatMessageNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('chat_message'), + chat_message: chatMessageSchema, +}); + +const emojiReactionNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('pleroma:emoji_reaction'), + emoji: emojiSchema, + emoji_url: z.string().url().optional().catch(undefined), +}); + +const eventReminderNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('pleroma:event_reminder'), + status: statusSchema, +}); + +const participationRequestNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('pleroma:participation_request'), + status: statusSchema, +}); + +const participationAcceptedNotificationSchema = baseNotificationSchema.extend({ + type: z.literal('pleroma:participation_accepted'), + status: statusSchema, +}); + +const notificationSchema = z.discriminatedUnion('type', [ + mentionNotificationSchema, + statusNotificationSchema, + reblogNotificationSchema, + followNotificationSchema, + followRequestNotificationSchema, + favouriteNotificationSchema, + pollNotificationSchema, + updateNotificationSchema, + moveNotificationSchema, + chatMessageNotificationSchema, + emojiReactionNotificationSchema, + eventReminderNotificationSchema, + participationRequestNotificationSchema, + participationAcceptedNotificationSchema, +]); + +type Notification = z.infer; + +export { notificationSchema, type Notification }; \ No newline at end of file diff --git a/app/soapbox/schemas/utils.ts b/app/soapbox/schemas/utils.ts index 5a62fa0c6..1e53e11aa 100644 --- a/app/soapbox/schemas/utils.ts +++ b/app/soapbox/schemas/utils.ts @@ -13,6 +13,9 @@ function filteredArray(schema: T) { )); } +/** Validates the string as an emoji. */ +const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)); + /** Map a list of CustomEmoji to their shortcodes. */ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) { return customEmojis.reduce>((result, emoji) => { @@ -21,4 +24,4 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) { }, {}); } -export { filteredArray, makeCustomEmojiMap }; \ No newline at end of file +export { filteredArray, makeCustomEmojiMap, emojiSchema }; \ No newline at end of file From 074c3c5b3986c8d46e4b224286b3345e39ffb305 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 11:26:31 -0500 Subject: [PATCH 2/7] Add attachmentSchema --- app/soapbox/schemas/attachment.ts | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/soapbox/schemas/attachment.ts diff --git a/app/soapbox/schemas/attachment.ts b/app/soapbox/schemas/attachment.ts new file mode 100644 index 000000000..44b9cb126 --- /dev/null +++ b/app/soapbox/schemas/attachment.ts @@ -0,0 +1,89 @@ +import { isBlurhashValid } from 'blurhash'; +import { z } from 'zod'; + +const blurhashSchema = z.string().superRefine((value, ctx) => { + const r = isBlurhashValid(value); + + if (!r.result) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: r.errorReason, + }); + } +}); + +const baseAttachmentSchema = z.object({ + blurhash: blurhashSchema.nullable().catch(null), + description: z.string().catch(''), + external_video_id: z.string().optional().catch(undefined), // TruthSocial + id: z.string(), + pleroma: z.object({ + mime_type: z.string().regex(/^\w+\/[-+.\w]+$/), + }).optional().catch(undefined), + preview_url: z.string().url().catch(''), + remote_url: z.string().url().nullable().catch(null), + type: z.string(), + url: z.string().url(), +}); + +const imageMetaSchema = z.object({ + width: z.number(), + height: z.number(), + aspect: z.number().optional().catch(undefined), +}).transform((meta) => ({ + ...meta, + aspect: typeof meta.aspect === 'number' ? meta.aspect : meta.width / meta.height, +})); + +const imageAttachmentSchema = baseAttachmentSchema.extend({ + type: z.literal('image'), + meta: z.object({ + original: imageMetaSchema.optional().catch(undefined), + }).catch({}), +}); + +const videoAttachmentSchema = baseAttachmentSchema.extend({ + type: z.literal('video'), + meta: z.object({ + duration: z.number().optional().catch(undefined), + original: imageMetaSchema.optional().catch(undefined), + }).catch({}), +}); + +const gifvAttachmentSchema = baseAttachmentSchema.extend({ + type: z.literal('gifv'), + meta: z.object({ + duration: z.number().optional().catch(undefined), + original: imageMetaSchema.optional().catch(undefined), + }).catch({}), +}); + +const audioAttachmentSchema = baseAttachmentSchema.extend({ + type: z.literal('audio'), + meta: z.object({ + duration: z.number().optional().catch(undefined), + }).catch({}), +}); + +const unknownAttachmentSchema = baseAttachmentSchema.extend({ + type: z.literal('unknown'), +}); + +/** https://docs.joinmastodon.org/entities/attachment */ +const attachmentSchema = z.discriminatedUnion('type', [ + imageAttachmentSchema, + videoAttachmentSchema, + gifvAttachmentSchema, + audioAttachmentSchema, + unknownAttachmentSchema, +]).transform((attachment) => { + if (!attachment.preview_url) { + attachment.preview_url = attachment.url; + } + + return attachment; +}); + +type Attachment = z.infer; + +export { attachmentSchema, type Attachment }; \ No newline at end of file From a7e1350a6505a700b3b23c5d58fd0b6c20dedd59 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 11:31:58 -0500 Subject: [PATCH 3/7] Add real chatMessageSchema --- app/soapbox/schemas/chat-message.ts | 22 +++++++++++++++++++--- app/soapbox/schemas/tag.ts | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/soapbox/schemas/chat-message.ts b/app/soapbox/schemas/chat-message.ts index a64ffec0b..121ef9d5f 100644 --- a/app/soapbox/schemas/chat-message.ts +++ b/app/soapbox/schemas/chat-message.ts @@ -1,9 +1,25 @@ import { z } from 'zod'; -import { normalizeChatMessage } from 'soapbox/normalizers'; -import { toSchema } from 'soapbox/utils/normalizers'; +import { attachmentSchema } from './attachment'; +import { cardSchema } from './card'; +import { customEmojiSchema } from './custom-emoji'; +import { emojiSchema, filteredArray } from './utils'; -const chatMessageSchema = toSchema(normalizeChatMessage); +const chatMessageSchema = z.object({ + account_id: z.string(), + media_attachments: filteredArray(attachmentSchema), + card: cardSchema.nullable().catch(null), + chat_id: z.string(), + content: z.string().catch(''), + created_at: z.string().datetime().catch(new Date().toUTCString()), + emojis: filteredArray(customEmojiSchema), + expiration: z.number().optional().catch(undefined), + emoji_reactions: z.array(emojiSchema).min(1).nullable().catch(null), + id: z.string(), + unread: z.coerce.boolean(), + deleting: z.coerce.boolean(), + pending: z.coerce.boolean(), +}); type ChatMessage = z.infer; diff --git a/app/soapbox/schemas/tag.ts b/app/soapbox/schemas/tag.ts index 5f74a31c7..22e903d60 100644 --- a/app/soapbox/schemas/tag.ts +++ b/app/soapbox/schemas/tag.ts @@ -5,7 +5,7 @@ const historySchema = z.object({ uses: z.coerce.number(), }); -/** // https://docs.joinmastodon.org/entities/tag */ +/** https://docs.joinmastodon.org/entities/tag */ const tagSchema = z.object({ name: z.string().min(1), url: z.string().url().catch(''), From 1dec42cd9fcb2e56c3dbb383a5d4c3bd453a09c2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 11:42:20 -0500 Subject: [PATCH 4/7] Add contentSchema helper --- app/soapbox/schemas/account.ts | 4 ++-- app/soapbox/schemas/chat-message.ts | 4 ++-- app/soapbox/schemas/utils.ts | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/soapbox/schemas/account.ts b/app/soapbox/schemas/account.ts index 9381edbbd..51202d2e6 100644 --- a/app/soapbox/schemas/account.ts +++ b/app/soapbox/schemas/account.ts @@ -5,7 +5,7 @@ import emojify from 'soapbox/features/emoji'; import { customEmojiSchema } from './custom-emoji'; import { relationshipSchema } from './relationship'; -import { filteredArray, makeCustomEmojiMap } from './utils'; +import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils'; const avatarMissing = require('assets/images/avatar-missing.png'); const headerMissing = require('assets/images/header-missing.png'); @@ -39,7 +39,7 @@ const accountSchema = z.object({ z.string(), z.null(), ]).catch(null), - note: z.string().catch(''), + note: contentSchema, pleroma: z.any(), // TODO source: z.any(), // TODO statuses_count: z.number().catch(0), diff --git a/app/soapbox/schemas/chat-message.ts b/app/soapbox/schemas/chat-message.ts index 121ef9d5f..fe0ff0f6b 100644 --- a/app/soapbox/schemas/chat-message.ts +++ b/app/soapbox/schemas/chat-message.ts @@ -3,14 +3,14 @@ import { z } from 'zod'; import { attachmentSchema } from './attachment'; import { cardSchema } from './card'; import { customEmojiSchema } from './custom-emoji'; -import { emojiSchema, filteredArray } from './utils'; +import { contentSchema, emojiSchema, filteredArray } from './utils'; const chatMessageSchema = z.object({ account_id: z.string(), media_attachments: filteredArray(attachmentSchema), card: cardSchema.nullable().catch(null), chat_id: z.string(), - content: z.string().catch(''), + content: contentSchema, created_at: z.string().datetime().catch(new Date().toUTCString()), emojis: filteredArray(customEmojiSchema), expiration: z.number().optional().catch(undefined), diff --git a/app/soapbox/schemas/utils.ts b/app/soapbox/schemas/utils.ts index 1e53e11aa..b64185800 100644 --- a/app/soapbox/schemas/utils.ts +++ b/app/soapbox/schemas/utils.ts @@ -2,6 +2,9 @@ import z from 'zod'; import type { CustomEmoji } from './custom-emoji'; +/** Ensure HTML content is a string, and drop empty `

` tags. */ +const contentSchema = z.string().catch('').transform((value) => value === '

' ? '' : value); + /** Validates individual items in an array, dropping any that aren't valid. */ function filteredArray(schema: T) { return z.any().array().catch([]) @@ -24,4 +27,4 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) { }, {}); } -export { filteredArray, makeCustomEmojiMap, emojiSchema }; \ No newline at end of file +export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema }; \ No newline at end of file From 9a6437568187f5338714cd6b1c987d0aae644e79 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 11:51:50 -0500 Subject: [PATCH 5/7] useGroupMedia: don't use statusSchema directly yet so we can change it --- app/soapbox/api/hooks/groups/useGroupMedia.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/soapbox/api/hooks/groups/useGroupMedia.ts b/app/soapbox/api/hooks/groups/useGroupMedia.ts index 23375bdc7..4db7fd179 100644 --- a/app/soapbox/api/hooks/groups/useGroupMedia.ts +++ b/app/soapbox/api/hooks/groups/useGroupMedia.ts @@ -1,7 +1,10 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks/useApi'; -import { statusSchema } from 'soapbox/schemas/status'; +import { normalizeStatus } from 'soapbox/normalizers'; +import { toSchema } from 'soapbox/utils/normalizers'; + +const statusSchema = toSchema(normalizeStatus); function useGroupMedia(groupId: string) { const api = useApi(); From e024e9212565731a92f21071bc3551284aee3a4c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 12:13:39 -0500 Subject: [PATCH 6/7] Add a real statusSchema --- app/soapbox/schemas/mention.ts | 18 +++++++++++ app/soapbox/schemas/status.ts | 57 ++++++++++++++++++++++++++++++++-- app/soapbox/schemas/utils.ts | 5 ++- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/schemas/mention.ts diff --git a/app/soapbox/schemas/mention.ts b/app/soapbox/schemas/mention.ts new file mode 100644 index 000000000..9bbdbff5b --- /dev/null +++ b/app/soapbox/schemas/mention.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +const mentionSchema = z.object({ + acct: z.string(), + id: z.string(), + url: z.string().url().catch(''), + username: z.string().catch(''), +}).transform((mention) => { + if (!mention.username) { + mention.username = mention.acct.split('@')[0]; + } + + return mention; +}); + +type Mention = z.infer; + +export { mentionSchema, type Mention }; \ No newline at end of file diff --git a/app/soapbox/schemas/status.ts b/app/soapbox/schemas/status.ts index 66d6f05eb..00fa6d483 100644 --- a/app/soapbox/schemas/status.ts +++ b/app/soapbox/schemas/status.ts @@ -1,9 +1,60 @@ import { z } from 'zod'; -import { normalizeStatus } from 'soapbox/normalizers'; -import { toSchema } from 'soapbox/utils/normalizers'; +import { accountSchema } from './account'; +import { attachmentSchema } from './attachment'; +import { cardSchema } from './card'; +import { customEmojiSchema } from './custom-emoji'; +import { groupSchema } from './group'; +import { mentionSchema } from './mention'; +import { pollSchema } from './poll'; +import { tagSchema } from './tag'; +import { contentSchema, dateSchema, filteredArray } from './utils'; -const statusSchema = toSchema(normalizeStatus); +const baseStatusSchema = z.object({ + account: accountSchema, + application: z.object({ + name: z.string(), + website: z.string().url().nullable().catch(null), + }).nullable().catch(null), + bookmarked: z.coerce.boolean(), + card: cardSchema.nullable().catch(null), + content: contentSchema, + created_at: dateSchema, + disliked: z.coerce.boolean(), + dislikes_count: z.number().catch(0), + edited_at: z.string().datetime().nullable().catch(null), + emojis: filteredArray(customEmojiSchema), + favourited: z.coerce.boolean(), + favourites_count: z.number().catch(0), + group: groupSchema.nullable().catch(null), + in_reply_to_account_id: z.string().nullable().catch(null), + in_reply_to_id: z.string().nullable().catch(null), + id: z.string(), + language: z.string().nullable().catch(null), + media_attachments: filteredArray(attachmentSchema), + mentions: filteredArray(mentionSchema), + muted: z.coerce.boolean(), + pinned: z.coerce.boolean(), + pleroma: z.object({}).optional().catch(undefined), + poll: pollSchema.nullable().catch(null), + quote: z.literal(null).catch(null), + quotes_count: z.number().catch(0), + reblog: z.literal(null).catch(null), + reblogged: z.coerce.boolean(), + reblogs_count: z.number().catch(0), + replies_count: z.number().catch(0), + sensitive: z.coerce.boolean(), + spoiler_text: contentSchema, + tags: filteredArray(tagSchema), + uri: z.string().url().catch(''), + url: z.string().url().catch(''), + visibility: z.string().catch('public'), +}); + +const statusSchema = baseStatusSchema.extend({ + quote: baseStatusSchema.nullable().catch(null), + reblog: baseStatusSchema.nullable().catch(null), +}); type Status = z.infer; diff --git a/app/soapbox/schemas/utils.ts b/app/soapbox/schemas/utils.ts index b64185800..c85b2b2b1 100644 --- a/app/soapbox/schemas/utils.ts +++ b/app/soapbox/schemas/utils.ts @@ -5,6 +5,9 @@ import type { CustomEmoji } from './custom-emoji'; /** Ensure HTML content is a string, and drop empty `

` tags. */ const contentSchema = z.string().catch('').transform((value) => value === '

' ? '' : value); +/** Validate to Mastodon's date format, or use the current date. */ +const dateSchema = z.string().datetime().catch(new Date().toUTCString()); + /** Validates individual items in an array, dropping any that aren't valid. */ function filteredArray(schema: T) { return z.any().array().catch([]) @@ -27,4 +30,4 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) { }, {}); } -export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema }; \ No newline at end of file +export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema }; \ No newline at end of file From 0e7ccd57ae7f8c725ad359129ed7906d823a99ba Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 4 May 2023 12:20:39 -0500 Subject: [PATCH 7/7] Export new schemas --- app/soapbox/schemas/account.ts | 2 +- app/soapbox/schemas/custom-emoji.ts | 2 +- app/soapbox/schemas/emoji-reaction.ts | 2 +- app/soapbox/schemas/group-member.ts | 2 +- app/soapbox/schemas/group-relationship.ts | 2 +- app/soapbox/schemas/index.ts | 5 +++++ 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/soapbox/schemas/account.ts b/app/soapbox/schemas/account.ts index 51202d2e6..919013329 100644 --- a/app/soapbox/schemas/account.ts +++ b/app/soapbox/schemas/account.ts @@ -121,4 +121,4 @@ const accountSchema = z.object({ type Account = z.infer; -export { accountSchema, Account }; \ No newline at end of file +export { accountSchema, type Account }; \ No newline at end of file diff --git a/app/soapbox/schemas/custom-emoji.ts b/app/soapbox/schemas/custom-emoji.ts index 68c49c587..1addf026a 100644 --- a/app/soapbox/schemas/custom-emoji.ts +++ b/app/soapbox/schemas/custom-emoji.ts @@ -14,4 +14,4 @@ const customEmojiSchema = z.object({ type CustomEmoji = z.infer; -export { customEmojiSchema, CustomEmoji }; +export { customEmojiSchema, type CustomEmoji }; diff --git a/app/soapbox/schemas/emoji-reaction.ts b/app/soapbox/schemas/emoji-reaction.ts index 28271fe29..1559148e1 100644 --- a/app/soapbox/schemas/emoji-reaction.ts +++ b/app/soapbox/schemas/emoji-reaction.ts @@ -11,4 +11,4 @@ const emojiReactionSchema = z.object({ type EmojiReaction = z.infer; -export { emojiReactionSchema, EmojiReaction }; \ No newline at end of file +export { emojiReactionSchema, type EmojiReaction }; \ No newline at end of file diff --git a/app/soapbox/schemas/group-member.ts b/app/soapbox/schemas/group-member.ts index 4521450cb..8135fecb6 100644 --- a/app/soapbox/schemas/group-member.ts +++ b/app/soapbox/schemas/group-member.ts @@ -16,4 +16,4 @@ const groupMemberSchema = z.object({ type GroupMember = z.infer; -export { groupMemberSchema, GroupMember, GroupRoles }; \ No newline at end of file +export { groupMemberSchema, type GroupMember, GroupRoles }; \ No newline at end of file diff --git a/app/soapbox/schemas/group-relationship.ts b/app/soapbox/schemas/group-relationship.ts index 5bf4cae31..baeb55a12 100644 --- a/app/soapbox/schemas/group-relationship.ts +++ b/app/soapbox/schemas/group-relationship.ts @@ -14,4 +14,4 @@ const groupRelationshipSchema = z.object({ type GroupRelationship = z.infer; -export { groupRelationshipSchema, GroupRelationship }; \ No newline at end of file +export { groupRelationshipSchema, type GroupRelationship }; \ No newline at end of file diff --git a/app/soapbox/schemas/index.ts b/app/soapbox/schemas/index.ts index 25f5f3d45..8e20c0d05 100644 --- a/app/soapbox/schemas/index.ts +++ b/app/soapbox/schemas/index.ts @@ -1,13 +1,18 @@ export { accountSchema, type Account } from './account'; +export { attachmentSchema, type Attachment } from './attachment'; export { cardSchema, type Card } from './card'; +export { chatMessageSchema, type ChatMessage } from './chat-message'; export { customEmojiSchema, type CustomEmoji } from './custom-emoji'; export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction'; export { groupSchema, type Group } from './group'; export { groupMemberSchema, type GroupMember } from './group-member'; export { groupRelationshipSchema, type GroupRelationship } from './group-relationship'; export { groupTagSchema, type GroupTag } from './group-tag'; +export { mentionSchema, type Mention } from './mention'; +export { notificationSchema, type Notification } from './notification'; export { pollSchema, type Poll, type PollOption } from './poll'; export { relationshipSchema, type Relationship } from './relationship'; +export { statusSchema, type Status } from './status'; export { tagSchema, type Tag } from './tag'; // Soapbox