import { NextRequest, NextResponse } from 'next/server'; import { marked } from 'marked'; import { prisma, ensureDefaultNotebook, getCurrentUserId } from '@/lib/prisma'; import { clipUrlToNote, getOrCreateTagIds, localizeClippedImages, } from '@/lib/importers'; import { saveAttachmentBuffer, stripHtmlToText, makeExcerpt, } from '@/lib/storage'; import { applyAutoSortToNote } from '@/lib/autoSort'; export const dynamic = 'force-dynamic'; function extractUrl(text: string): { extractedUrl: string | null; remainingText: string } { if (!text) return { extractedUrl: null, remainingText: '' }; const match = /(https?:\/\/[^\s]+)/i.exec(text); if (!match) return { extractedUrl: null, remainingText: text }; const extractedUrl = match[1]; const remainingText = text.replace(extractedUrl, '').trim(); return { extractedUrl, remainingText }; } function getRedirectUrl(req: NextRequest, noteId?: string, isError?: boolean): URL { const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || 'everclone.vloser.us'; const proto = req.headers.get('x-forwarded-proto') || 'https'; if (isError) { return new URL(`${proto}://${host}/?shared_error=1`); } return new URL(`${proto}://${host}/?noteId=${noteId}&shared=1`); } export async function GET(req: NextRequest) { try { const userId = await getCurrentUserId(); const defaultNb = await ensureDefaultNotebook(userId); const sp = req.nextUrl.searchParams; let title = (sp.get('title') || '').trim(); let text = (sp.get('text') || sp.get('content') || '').trim(); let url = (sp.get('url') || '').trim(); // Check if URL was shared inside text or title if (!url && text) { const extracted = extractUrl(text); if (extracted.extractedUrl) { url = extracted.extractedUrl; text = extracted.remainingText; } } if (!url && title) { const extracted = extractUrl(title); if (extracted.extractedUrl) { url = extracted.extractedUrl; title = extracted.remainingText; } } if (!url && title && /^https?:\/\/\S+$/i.test(title)) { url = title; title = ''; } if (url) { url = url.replace(/[.,;:)\]]+$/, ''); } if (url && text) { const remaining = text.replace(url, '').trim(); if ( !remaining || remaining === title || title.includes(remaining) || remaining.includes(title) || (!remaining.includes('\n') && remaining.length < 300) ) { if (!title && remaining) { title = remaining; } text = ''; } } if (url) { const recentDup = await prisma.note.findFirst({ where: { userId, sourceUrl: url, createdAt: { gte: new Date(Date.now() - 5 * 60 * 1000), }, }, orderBy: { createdAt: 'desc' }, }); if (recentDup) { return NextResponse.redirect(getRedirectUrl(req, recentDup.id), 303); } } let noteId: string; if (url && !text) { // Direct web clip noteId = await clipUrlToNote(url, { title: title || undefined, notebookId: defaultNb.id, tags: ['inbox', 'web-clip'], }); } else { const contentHtml = text ? `
${text.replace(/\n/g, '
')}
Source: ${url}
` : '' }` : ``; const tagIds = await getOrCreateTagIds(['inbox', 'share'], userId); const created = await prisma.note.create({ data: { title: title || url || 'Shared Note', contentHtml, contentText: stripHtmlToText(contentHtml), excerpt: makeExcerpt(stripHtmlToText(contentHtml)), notebookId: defaultNb.id, sourceUrl: url || null, tags: { create: tagIds.map((tagId) => ({ tagId })), }, }, }); noteId = created.id; } await applyAutoSortToNote(noteId, userId).catch(() => {}); return NextResponse.redirect(getRedirectUrl(req, noteId), 303); } catch (err) { console.error('GET /share error:', err); return NextResponse.redirect(getRedirectUrl(req, undefined, true), 303); } } export async function POST(req: NextRequest) { try { const userId = await getCurrentUserId(); const defaultNb = await ensureDefaultNotebook(userId); const contentType = req.headers.get('content-type') || ''; let title = ''; let text = ''; let url = ''; const uploadedFiles: File[] = []; if (contentType.includes('application/json')) { const body = await req.json(); title = (body.title || '').trim(); text = (body.text || body.content || '').trim(); url = (body.url || '').trim(); } else { const formData = await req.formData(); title = ((formData.get('title') as string) || '').trim(); text = ( (formData.get('text') as string) || (formData.get('content') as string) || '' ).trim(); url = ((formData.get('url') as string) || '').trim(); for (const [key, value] of formData.entries()) { if (value && typeof value === 'object' && 'arrayBuffer' in value) { const file = value as File; if (file.size > 0) { uploadedFiles.push(file); } } } } // Check if Android shared a URL inside `text` or `title` if (!url && text) { const extracted = extractUrl(text); if (extracted.extractedUrl) { url = extracted.extractedUrl; text = extracted.remainingText; } } if (!url && title) { const extracted = extractUrl(title); if (extracted.extractedUrl) { url = extracted.extractedUrl; title = extracted.remainingText; } } if (!url && title && /^https?:\/\/\S+$/i.test(title)) { url = title; title = ''; } if (url) { url = url.replace(/[.,;:)\]]+$/, ''); } if (url && text) { const remaining = text.replace(url, '').trim(); if ( !remaining || remaining === title || title.includes(remaining) || remaining.includes(title) || (!remaining.includes('\n') && remaining.length < 300) ) { if (!title && remaining) { title = remaining; } text = ''; } } if (url) { const recentDup = await prisma.note.findFirst({ where: { userId, sourceUrl: url, createdAt: { gte: new Date(Date.now() - 5 * 60 * 1000), }, }, orderBy: { createdAt: 'desc' }, }); if (recentDup) { return NextResponse.redirect(getRedirectUrl(req, recentDup.id), 303); } } const wantsJson = contentType.includes('application/json') || Boolean(req.headers.get('accept')?.includes('application/json')); // 1. If sharing a URL with no uploaded files and no extra body text, clip the webpage if (url && !text && uploadedFiles.length === 0) { try { const clippedId = await clipUrlToNote(url, { title: title || undefined, notebookId: defaultNb.id, tags: ['inbox', 'web-clip'], }); await applyAutoSortToNote(clippedId, userId).catch(() => {}); if (wantsJson) { return NextResponse.json({ ok: true, noteId: clippedId, mode: 'web-clip' }, { status: 201 }); } return NextResponse.redirect(getRedirectUrl(req, clippedId), 303); } catch (e) { console.warn('URL clipping failed in /share, falling back to basic link note:', e); } } // 2. Generate base HTML content let finalHtml = ''; if (text) { finalHtml = await marked.parse(text, { gfm: true, breaks: true }); } if (url && !finalHtml.includes(url)) { finalHtml = `${finalHtml ? `${finalHtml}\n` : ''}Source: ${url}
`; } if (!finalHtml && uploadedFiles.length === 0) { finalHtml = ''; } const finalTitle = title || stripHtmlToText(finalHtml).slice(0, 60).trim() || (uploadedFiles[0]?.name ? `Attachment: ${uploadedFiles[0].name}` : 'Shared Note'); const tagNames = ['inbox', 'share']; if (url && !text && uploadedFiles.length === 0) { tagNames.push('needs-reclip'); } const tagIds = await getOrCreateTagIds(tagNames, userId); const note = await prisma.note.create({ data: { title: finalTitle, contentHtml: finalHtml, contentText: stripHtmlToText(finalHtml), excerpt: makeExcerpt(stripHtmlToText(finalHtml)), notebookId: defaultNb.id, sourceUrl: url || null, tags: { create: tagIds.map((tagId) => ({ tagId })), }, }, }); // 3. Save any uploaded attachments (images, PDFs, audio, documents) let appendedMediaHtml = ''; for (const f of uploadedFiles) { const buf = Buffer.from(await f.arrayBuffer()); const mimeType = f.type || 'application/octet-stream'; const filename = f.name || 'shared-file'; const savedBlob = await saveAttachmentBuffer(buf, filename, mimeType); const att = await prisma.attachment.create({ data: { noteId: note.id, filename, mimeType, sizeBytes: savedBlob.sizeBytes, md5Hash: savedBlob.md5Hash, storagePath: savedBlob.storagePath, extractedText: savedBlob.extractedText, }, }); if (mimeType.startsWith('image/')) { appendedMediaHtml += `