Support imeta tags

This commit is contained in:
Alex Gleason 2024-05-18 13:22:20 -05:00
parent c8f9483795
commit 7d34b9401e
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
6 changed files with 46 additions and 49 deletions

View file

@ -96,7 +96,7 @@ const createStatusController: AppController = async (c) => {
if (data.media_ids?.length) { if (data.media_ids?.length) {
const media = await getUnattachedMediaByIds(kysely, data.media_ids) const media = await getUnattachedMediaByIds(kysely, data.media_ids)
.then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) .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); tags.push(...media);
} }

View file

@ -3,13 +3,12 @@ import uuid62 from 'uuid62';
import { DittoDB } from '@/db/DittoDB.ts'; import { DittoDB } from '@/db/DittoDB.ts';
import { DittoTables } from '@/db/DittoTables.ts'; import { DittoTables } from '@/db/DittoTables.ts';
import { type MediaData } from '@/schemas/nostr.ts';
interface UnattachedMedia { interface UnattachedMedia {
id: string; id: string;
pubkey: string; pubkey: string;
url: string; url: string;
data: MediaData; data: string[][]; // NIP-94 tags
uploaded_at: number; uploaded_at: number;
} }

View file

@ -9,27 +9,12 @@ const signedEventSchema = n.event()
.refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash')
.refine(verifyEvent, 'Event signature is invalid'); .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. */ /** Kind 0 content schema for the Ditto server admin user. */
const serverMetaSchema = n.metadata().and(z.object({ const serverMetaSchema = n.metadata().and(z.object({
tagline: z.string().optional().catch(undefined), tagline: z.string().optional().catch(undefined),
email: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined),
})); }));
/** Media data from `"media"` tags. */
type MediaData = z.infer<typeof mediaDataSchema>;
/** NIP-11 Relay Information Document. */ /** NIP-11 Relay Information Document. */
const relayInfoDocSchema = z.object({ const relayInfoDocSchema = z.object({
name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), 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. */ /** NIP-30 custom emoji tag. */
type EmojiTag = z.infer<typeof emojiTagSchema>; type EmojiTag = z.infer<typeof emojiTagSchema>;
export { export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema };
type EmojiTag,
emojiTagSchema,
type MediaData,
mediaDataSchema,
relayInfoDocSchema,
serverMetaSchema,
signedEventSchema,
};

View file

@ -8,26 +8,37 @@ interface FileMeta {
} }
/** Upload a file, track it in the database, and return the resulting media object. */ /** Upload a file, track it in the database, and return the resulting media object. */
async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal) { async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Promise<string[][]> {
const { name, type, size } = file; const { type, size } = file;
const { pubkey, description } = meta; const { pubkey, description } = meta;
if (file.size > Conf.maxUploadSize) { if (file.size > Conf.maxUploadSize) {
throw new Error('File size is too large.'); 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({ const data: string[][] = [
pubkey, ['url', url],
url, ['m', type],
data: { ['size', size.toString()],
name, ];
size,
description, if (sha256) {
mime: type, 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 }; export { uploadFile };

View file

@ -6,15 +6,21 @@ type DittoAttachment = TypeFest.SetOptional<UnattachedMedia, 'id' | 'pubkey' | '
function renderAttachment(media: DittoAttachment) { function renderAttachment(media: DittoAttachment) {
const { id, data, url } = media; const { id, data, url } = media;
const m = data.find(([name]) => 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 { return {
id: id ?? url ?? data.cid, id: id ?? url,
type: getAttachmentType(data.mime ?? ''), type: getAttachmentType(m ?? ''),
url, url,
preview_url: url, preview_url: url,
remote_url: null, remote_url: null,
description: data.description ?? '', description: alt ?? '',
blurhash: data.blurhash || null, blurhash: blurhash || null,
cid: data.cid, cid: cid,
}; };
} }

View file

@ -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 { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
@ -12,7 +12,6 @@ import { unfurlCardCached } from '@/utils/unfurl.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
import { mediaDataSchema } from '@/schemas/nostr.ts';
interface RenderStatusOpts { interface RenderStatusOpts {
viewerPubkey?: string; viewerPubkey?: string;
@ -80,8 +79,13 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
const mediaLinks = getMediaLinks(links); const mediaLinks = getMediaLinks(links);
const mediaTags: DittoAttachment[] = event.tags const mediaTags: DittoAttachment[] = event.tags
.filter((tag) => tag[0] === 'media') .filter(([name]) => name === 'imeta')
.map(([_, url, json]) => ({ url, data: n.json().pipe(mediaDataSchema).parse(json) })); .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]; const media = [...mediaLinks, ...mediaTags];