From 7d34b9401e8b191e91801d205c85c3685a876769 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 13:22:20 -0500 Subject: [PATCH] Support imeta tags --- src/controllers/api/statuses.ts | 2 +- src/db/unattached-media.ts | 3 +-- src/schemas/nostr.ts | 25 +-------------------- src/upload.ts | 37 ++++++++++++++++++++----------- src/views/mastodon/attachments.ts | 16 ++++++++----- src/views/mastodon/statuses.ts | 12 ++++++---- 6 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 00f9a98..8904b2b 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -96,7 +96,7 @@ const createStatusController: AppController = async (c) => { if (data.media_ids?.length) { const media = await getUnattachedMediaByIds(kysely, data.media_ids) .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) - .then((media) => media.map(({ url, data }) => ['media', url, data])); + .then((media) => media.map(({ data }) => ['imeta', ...data])); tags.push(...media); } diff --git a/src/db/unattached-media.ts b/src/db/unattached-media.ts index cee1e3a..0628278 100644 --- a/src/db/unattached-media.ts +++ b/src/db/unattached-media.ts @@ -3,13 +3,12 @@ import uuid62 from 'uuid62'; import { DittoDB } from '@/db/DittoDB.ts'; import { DittoTables } from '@/db/DittoTables.ts'; -import { type MediaData } from '@/schemas/nostr.ts'; interface UnattachedMedia { id: string; pubkey: string; url: string; - data: MediaData; + data: string[][]; // NIP-94 tags uploaded_at: number; } diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index a42b9f0..d8aa29a 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -9,27 +9,12 @@ const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); -/** Media data schema from `"media"` tags. */ -const mediaDataSchema = z.object({ - blurhash: z.string().optional().catch(undefined), - cid: z.string().optional().catch(undefined), - description: z.string().max(200).optional().catch(undefined), - height: z.number().int().positive().optional().catch(undefined), - mime: z.string().optional().catch(undefined), - name: z.string().optional().catch(undefined), - size: z.number().int().positive().optional().catch(undefined), - width: z.number().int().positive().optional().catch(undefined), -}); - /** Kind 0 content schema for the Ditto server admin user. */ const serverMetaSchema = n.metadata().and(z.object({ tagline: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined), })); -/** Media data from `"media"` tags. */ -type MediaData = z.infer; - /** NIP-11 Relay Information Document. */ const relayInfoDocSchema = z.object({ name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), @@ -47,12 +32,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() /** NIP-30 custom emoji tag. */ type EmojiTag = z.infer; -export { - type EmojiTag, - emojiTagSchema, - type MediaData, - mediaDataSchema, - relayInfoDocSchema, - serverMetaSchema, - signedEventSchema, -}; +export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema }; diff --git a/src/upload.ts b/src/upload.ts index 632dbab..4f5fd14 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -8,26 +8,37 @@ interface FileMeta { } /** Upload a file, track it in the database, and return the resulting media object. */ -async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal) { - const { name, type, size } = file; +async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise { + const { type, size } = file; const { pubkey, description } = meta; if (file.size > Conf.maxUploadSize) { throw new Error('File size is too large.'); } - const { url } = await uploader.upload(file, { signal }); + const { url, sha256, cid } = await uploader.upload(file, { signal }); - return insertUnattachedMedia({ - pubkey, - url, - data: { - name, - size, - description, - mime: type, - }, - }); + const data: string[][] = [ + ['url', url], + ['m', type], + ['size', size.toString()], + ]; + + if (sha256) { + data.push(['x', sha256]); + } + + if (cid) { + data.push(['cid', cid]); + } + + if (description) { + data.push(['alt', description]); + } + + await insertUnattachedMedia({ pubkey, url, data }); + + return data; } export { uploadFile }; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 3ea989e..18fe031 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -6,15 +6,21 @@ type DittoAttachment = TypeFest.SetOptional name === 'm')?.[1]; + const alt = data.find(([name]) => name === 'alt')?.[1]; + const cid = data.find(([name]) => name === 'cid')?.[1]; + const blurhash = data.find(([name]) => name === 'blurhash')?.[1]; + return { - id: id ?? url ?? data.cid, - type: getAttachmentType(data.mime ?? ''), + id: id ?? url, + type: getAttachmentType(m ?? ''), url, preview_url: url, remote_url: null, - description: data.description ?? '', - blurhash: data.blurhash || null, - cid: data.cid, + description: alt ?? '', + blurhash: blurhash || null, + cid: cid, }; } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 1b0ebe8..8428e9a 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent } from '@nostrify/nostrify'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { nip19 } from 'nostr-tools'; @@ -12,7 +12,6 @@ import { unfurlCardCached } from '@/utils/unfurl.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; -import { mediaDataSchema } from '@/schemas/nostr.ts'; interface RenderStatusOpts { viewerPubkey?: string; @@ -80,8 +79,13 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< const mediaLinks = getMediaLinks(links); const mediaTags: DittoAttachment[] = event.tags - .filter((tag) => tag[0] === 'media') - .map(([_, url, json]) => ({ url, data: n.json().pipe(mediaDataSchema).parse(json) })); + .filter(([name]) => name === 'imeta') + .map(([_, ...entries]) => { + const data = entries.map((entry) => entry.split(' ')); + const url = data.find(([name]) => name === 'url')?.[1]; + return { url, data }; + }) + .filter((media): media is DittoAttachment => !!media.url); const media = [...mediaLinks, ...mediaTags];