diff --git a/src/apps/images/index.tsx b/src/apps/images/index.tsx index 0015afb..6d82e14 100644 --- a/src/apps/images/index.tsx +++ b/src/apps/images/index.tsx @@ -163,8 +163,13 @@ function PictureComposer({ onPublished }: { onPublished: () => void }) { const publishPicture = async () => { if (!file) return; try { - const tags = await upload.mutateAsync(file); - await publish.mutateAsync({ kind: 20, content: description.trim(), tags }); + const uploadTags = await upload.mutateAsync(file); + const url = uploadTags.find(([name, value]) => name === 'url' && value)?.[1]; + if (!url) throw new Error('Upload did not return a URL.'); + const imeta = ['imeta', `url ${url}`, ...uploadTags + .filter(([name, value]) => name !== 'url' && value) + .map(([name, value]) => `${name} ${value}`)]; + await publish.mutateAsync({ kind: 20, content: description.trim(), tags: [imeta] }); setFile(null); setDescription(''); toast({ title: 'Picture published' }); onPublished(); } catch (error) { toast({ title: 'Could not publish picture', description: error instanceof Error ? error.message : 'Upload failed.', variant: 'destructive' }); } }; diff --git a/src/lib/picturePosts.test.ts b/src/lib/picturePosts.test.ts index f8ea033..bfe4a9d 100644 --- a/src/lib/picturePosts.test.ts +++ b/src/lib/picturePosts.test.ts @@ -17,5 +17,7 @@ describe('picture posts', () => { it('rejects non-picture kinds and unsafe image URLs', () => { expect(isPicturePost(event(1, [['imeta', 'url https://images.example/picture.jpg']]))).toBe(false); expect(isPicturePost(event(20, [['imeta', 'url javascript:alert(1)']]))).toBe(false); + expect(isPicturePost(event(20, [['imeta', 'url mailto:image@example.com']]))).toBe(false); + expect(isPicturePost(event(20, [['imeta', 'url /picture.jpg']]))).toBe(false); }); }); diff --git a/src/lib/picturePosts.ts b/src/lib/picturePosts.ts index 0141da9..41a0cf4 100644 --- a/src/lib/picturePosts.ts +++ b/src/lib/picturePosts.ts @@ -7,7 +7,15 @@ export function pictureUrl(event: NostrEvent): string | undefined { const metadata = event.tags.find(([name]) => name === 'imeta'); const url = metadata?.find((value) => value.startsWith('url '))?.slice(4); - return sanitizeUrl(url); + if (!url) return undefined; + + try { + const parsed = new URL(url); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined; + return sanitizeUrl(parsed.href); + } catch { + return undefined; + } } /** Picture posts are Kind 20 events with an image the client can safely render. */