/** * POST /api/integrations/readwise/sync * Syncs Readwise highlights into Momento notes. * Each book/article becomes a note with all its highlights listed. * * Query params: * ?token=xxx (Readwise API token) — used for initial test; stored in UserAISettings.integrationTokens */ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import prisma from '@/lib/prisma' const READWISE_API = 'https://readwise.io/api/v2' interface ReadwiseHighlight { id: number text: string note: string location: number book_id: number highlighted_at: string | null url: string | null color: string } interface ReadwiseBook { id: number title: string author: string | null category: string source: string num_highlights: number last_highlight_at: string | null cover_image_url: string | null source_url: string | null } interface ReadwisePaginatedResponse { count: number next: string | null results: T[] } async function fetchAllPages(url: string, token: string): Promise { const results: T[] = [] let nextUrl: string | null = url while (nextUrl) { const res = await fetch(nextUrl, { headers: { Authorization: `Token ${token}` }, }) if (!res.ok) throw new Error(`Readwise API error: ${res.status}`) const data: ReadwisePaginatedResponse = await res.json() results.push(...data.results) nextUrl = data.next } return results } function buildNoteContent(book: ReadwiseBook, highlights: ReadwiseHighlight[]): string { const sourceLink = book.source_url ? `

${book.source_url}

` : '' const highlightLines = highlights .sort((a, b) => a.location - b.location) .map((h) => { const note = h.note ? `
${h.note}
` : '' return `
  • ${h.text}${note}
  • ` }) .join('\n') return `

    📚 ${book.title}

    ${book.author ? `

    par ${book.author}

    ` : ''} ${sourceLink}

    ${highlights.length} surlignage${highlights.length > 1 ? 's' : ''}

      ${highlightLines}
    ` } export async function POST(req: NextRequest) { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id // Retrieve stored Readwise token from UserAISettings metadata const aiSettings = await prisma.userAISettings.findUnique({ where: { userId }, select: { integrationTokens: true }, }) let token: string | undefined try { const meta = typeof aiSettings?.integrationTokens === 'string' ? JSON.parse(aiSettings.integrationTokens) : (aiSettings?.integrationTokens as Record | null) ?? {} token = meta?.readwiseToken as string | undefined } catch { token = undefined } // Allow passing token directly (for test/setup) const body = await req.json().catch(() => ({})) if (body.token && typeof body.token === 'string') { token = body.token // Persist token await prisma.userAISettings.upsert({ where: { userId }, update: { integrationTokens: JSON.stringify({ ...(typeof aiSettings?.integrationTokens === 'string' ? JSON.parse(aiSettings.integrationTokens) : {}), readwiseToken: body.token, }), }, create: { userId, integrationTokens: { readwiseToken: body.token }, }, }) } if (!token) { return NextResponse.json({ error: 'Readwise token not configured' }, { status: 400 }) } try { // Fetch books and highlights const [books, highlights] = await Promise.all([ fetchAllPages(`${READWISE_API}/books/?page_size=100`, token), fetchAllPages(`${READWISE_API}/highlights/?page_size=500`, token), ]) // Group highlights by book const byBook = new Map() for (const h of highlights) { const arr = byBook.get(h.book_id) ?? [] arr.push(h) byBook.set(h.book_id, arr) } // Find or create a "Readwise" notebook let notebook = await prisma.notebook.findFirst({ where: { userId, name: 'Readwise' }, }) if (!notebook) { notebook = await prisma.notebook.create({ data: { userId, name: 'Readwise', icon: '📚', order: 999 }, }) } let created = 0 let updated = 0 for (const book of books) { const bookHighlights = byBook.get(book.id) ?? [] if (bookHighlights.length === 0) continue const content = buildNoteContent(book, bookHighlights) const existing = await prisma.note.findFirst({ where: { userId, notebookId: notebook.id, title: book.title, trashedAt: null }, }) if (existing) { await prisma.note.update({ where: { id: existing.id }, data: { content, updatedAt: new Date() }, }) updated++ } else { await prisma.note.create({ data: { userId, notebookId: notebook.id, title: book.title, content, labels: JSON.stringify(['readwise', book.category]), }, }) created++ } } return NextResponse.json({ success: true, created, updated, notebookId: notebook.id, books: books.length, }) } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' console.error('[readwise/sync]', message) return NextResponse.json({ error: message }, { status: 500 }) } } /** * GET /api/integrations/readwise/sync * Returns Readwise connection status (token configured or not). */ export async function GET() { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const aiSettings = await prisma.userAISettings.findUnique({ where: { userId: session.user.id }, select: { integrationTokens: true }, }) let connected = false try { const meta = typeof aiSettings?.integrationTokens === 'string' ? JSON.parse(aiSettings.integrationTokens) : (aiSettings?.integrationTokens as Record | null) ?? {} connected = !!(meta?.readwiseToken) } catch { connected = false } return NextResponse.json({ connected }) } /** * DELETE /api/integrations/readwise/sync * Removes the stored Readwise token. */ export async function DELETE() { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id const aiSettings = await prisma.userAISettings.findUnique({ where: { userId }, select: { integrationTokens: true }, }) if (aiSettings) { try { const meta = ((aiSettings.integrationTokens as Record | null) ?? {}) delete meta.readwiseToken await prisma.userAISettings.update({ where: { userId }, data: { integrationTokens: JSON.stringify(meta) }, }) } catch { /* no-op */ } } return NextResponse.json({ success: true }) }