Rich media link previews
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
let url = searchParams.get('url');
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: 'No URL provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Normalize URL - handle synapse.social etc
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'SynapsisBot/1.0',
|
||||
},
|
||||
signal: AbortSignal.timeout(5000), // 5s timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch URL: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Simple regex extraction for OG tags
|
||||
const getMeta = (property: string) => {
|
||||
const regex = new RegExp(`<meta[^>]+(?:property|name)=["'](?:og:)?${property}["'][^>]+content=["']([^"']+)["']`, 'i');
|
||||
const match = html.match(regex);
|
||||
if (match) return match[1];
|
||||
|
||||
// Try different order
|
||||
const regexRev = new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:property|name)=["'](?:og:)?${property}["']`, 'i');
|
||||
const matchRev = html.match(regexRev);
|
||||
return matchRev ? matchRev[1] : null;
|
||||
};
|
||||
|
||||
const title = getMeta('title') || html.match(/<title>([^<]+)<\/title>/i)?.[1];
|
||||
const description = getMeta('description');
|
||||
const image = getMeta('image');
|
||||
|
||||
return NextResponse.json({
|
||||
url,
|
||||
title: title?.trim() || url,
|
||||
description: description?.trim() || null,
|
||||
image: image?.trim() || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Link preview error:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ const createPostSchema = z.object({
|
||||
content: z.string().min(1).max(POST_MAX_LENGTH),
|
||||
replyToId: z.string().uuid().optional(),
|
||||
mediaIds: z.array(z.string().uuid()).max(4).optional(),
|
||||
linkPreview: z.object({
|
||||
url: z.string().url(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
image: z.string().url().optional().nullable(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
// Create a new post
|
||||
@@ -41,6 +47,10 @@ export async function POST(request: Request) {
|
||||
replyToId: data.replyToId,
|
||||
apId: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
|
||||
apUrl: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
|
||||
linkPreviewUrl: data.linkPreview?.url,
|
||||
linkPreviewTitle: data.linkPreview?.title,
|
||||
linkPreviewDescription: data.linkPreview?.description,
|
||||
linkPreviewImage: data.linkPreview?.image,
|
||||
}).returning();
|
||||
|
||||
let attachedMedia: typeof media.$inferSelect[] = [];
|
||||
@@ -276,7 +286,7 @@ export async function GET(request: Request) {
|
||||
|
||||
// Get posts from people the user follows + their own posts
|
||||
// For now, just return all posts (we'll add following filter later)
|
||||
feedPosts = await db.query.posts.findMany({
|
||||
feedPosts = await db.query.posts.findMany({
|
||||
where: baseFilter,
|
||||
with: {
|
||||
author: true,
|
||||
|
||||
Reference in New Issue
Block a user