← back to Dear Bubbe Nextjs

app/api/jewish-news/route.ts

295 lines

import { NextResponse } from 'next/server'
import jewishNewsFeeds from '@/config/jewish-news-feeds.json'

// Simple XML to JSON parser for RSS
function parseRSS(xml: string) {
  const items: any[] = []
  
  // Extract channel info
  const channelMatch = xml.match(/<channel>([\s\S]*?)<\/channel>/i)
  if (!channelMatch) return items
  
  // Extract all items
  const itemMatches = xml.matchAll(/<item>([\s\S]*?)<\/item>/gi)
  
  for (const match of itemMatches) {
    const itemXml = match[1]
    
    const title = itemXml.match(/<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/i)?.[1] || ''
    const link = itemXml.match(/<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/link>/i)?.[1] || ''
    const description = itemXml.match(/<description>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/description>/i)?.[1] || ''
    const pubDate = itemXml.match(/<pubDate>(.*?)<\/pubDate>/i)?.[1] || ''
    
    // Clean up CDATA sections
    const cleanTitle = title.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1').replace(/<[^>]+>/g, '').trim()
    const cleanDescription = description.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1').replace(/<[^>]+>/g, '').trim()
    const cleanLink = link.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1').trim()
    
    if (cleanTitle) {
      items.push({
        title: cleanTitle,
        link: cleanLink,
        description: cleanDescription.substring(0, 200),
        published: pubDate,
      })
    }
  }
  
  return items
}

async function fetchFeed(url: string, name: string, category: string) {
  try {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout
    
    const response = await fetch(url, {
      signal: controller.signal,
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; BubbeAI/1.0; +https://bubbe.ai)',
        'Accept': 'application/rss+xml, application/xml, text/xml, */*'
      }
    })
    
    clearTimeout(timeoutId)
    
    if (!response.ok) {
      console.error(`Failed to fetch ${name}: ${response.status}`)
      return []
    }
    
    const text = await response.text()
    const items = parseRSS(text)
    
    return items.slice(0, 3).map(item => ({
      ...item,
      source: name,
      category
    }))
  } catch (error) {
    console.error(`Error fetching ${name}:`, error)
    return []
  }
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const location = searchParams.get('location') || ''
  const topics = searchParams.get('topics')?.split(',') || ['world', 'torah']
  const quick = searchParams.get('quick') === 'true'
  
  try {
    const results: any[] = []
    const promises: Promise<any[]>[] = []
    
    // Fetch global feeds based on topics
    if (topics.includes('world') && jewishNewsFeeds.global.world) {
      const worldFeeds = quick 
        ? jewishNewsFeeds.global.world.slice(0, 2)
        : jewishNewsFeeds.global.world.slice(0, 4)
      
      for (const feed of worldFeeds) {
        promises.push(fetchFeed(feed.url, feed.name, 'world'))
      }
    }
    
    if (topics.includes('security') && jewishNewsFeeds.global.security) {
      const securityFeeds = jewishNewsFeeds.global.security.slice(0, 2)
      for (const feed of securityFeeds) {
        promises.push(fetchFeed(feed.url, feed.name, 'security'))
      }
    }
    
    if (topics.includes('torah') && jewishNewsFeeds.global.torah) {
      const torahFeeds = quick
        ? jewishNewsFeeds.global.torah.slice(0, 1)  
        : jewishNewsFeeds.global.torah.slice(0, 2)
      
      for (const feed of torahFeeds) {
        promises.push(fetchFeed(feed.url, feed.name, 'torah'))
      }
    }
    
    if (topics.includes('culture') && jewishNewsFeeds.global.culture) {
      const cultureFeeds = jewishNewsFeeds.global.culture.slice(0, 2)
      for (const feed of cultureFeeds) {
        promises.push(fetchFeed(feed.url, feed.name, 'culture'))
      }
    }
    
    // Fetch local feeds if location provided
    if (location && topics.includes('local')) {
      const localKey = location.toLowerCase().replace(/\s+/g, '_')
      const localConfig = (jewishNewsFeeds.local as any)[localKey]
      
      if (localConfig && localConfig.feeds) {
        for (const feed of localConfig.feeds.slice(0, 2)) {
          promises.push(fetchFeed(feed.url, feed.name, 'local'))
        }
      }
    }
    
    // Wait for all feeds with timeout
    const allResults = await Promise.allSettled(promises)
    
    for (const result of allResults) {
      if (result.status === 'fulfilled' && result.value) {
        results.push(...result.value)
      }
    }
    
    // Group by category
    const grouped: Record<string, any[]> = {}
    for (const item of results) {
      if (!grouped[item.category]) {
        grouped[item.category] = []
      }
      grouped[item.category].push(item)
    }
    
    // Generate Bubbe's summary with sources
    let bubbeResponse = ''
    let bubbeResponseWithLinks = ''
    const intros = jewishNewsFeeds.keywords.bubbe_responses.intro
    const introLine = intros[Math.floor(Math.random() * intros.length)]
    bubbeResponse = introLine + '\n\n'
    bubbeResponseWithLinks = introLine + '\n\n'
    
    if (grouped.world && grouped.world.length > 0) {
      bubbeResponse += '**WORLD NEWS:**\n'
      bubbeResponseWithLinks += '**WORLD NEWS:**\n'
      grouped.world.slice(0, 3).forEach((item, index) => {
        bubbeResponse += `• ${item.title}\n`
        if (item.link) {
          bubbeResponseWithLinks += `• [${item.title}](${item.link}) _(${item.source})_\n`
        } else {
          bubbeResponseWithLinks += `• ${item.title} _(${item.source})_\n`
        }
      })
      bubbeResponse += '\n'
      bubbeResponseWithLinks += '\n'
    }
    
    if (grouped.security && grouped.security.length > 0) {
      const alerts = jewishNewsFeeds.keywords.bubbe_responses.security_alert
      const alertLine = alerts[Math.floor(Math.random() * alerts.length)]
      bubbeResponse += alertLine + '\n'
      bubbeResponseWithLinks += alertLine + '\n'
      bubbeResponse += '**SECURITY:**\n'
      bubbeResponseWithLinks += '**SECURITY:**\n'
      grouped.security.slice(0, 2).forEach((item, index) => {
        bubbeResponse += `• ${item.title}\n`
        if (item.link) {
          bubbeResponseWithLinks += `• [${item.title}](${item.link}) _(${item.source})_\n`
        } else {
          bubbeResponseWithLinks += `• ${item.title} _(${item.source})_\n`
        }
      })
      bubbeResponse += '\n'
      bubbeResponseWithLinks += '\n'
    }
    
    if (grouped.torah && grouped.torah.length > 0) {
      const torahIntros = jewishNewsFeeds.keywords.bubbe_responses.torah
      const torahLine = torahIntros[Math.floor(Math.random() * torahIntros.length)]
      bubbeResponse += torahLine + '\n'
      bubbeResponseWithLinks += torahLine + '\n'
      bubbeResponse += '**TORAH & LEARNING:**\n'
      bubbeResponseWithLinks += '**TORAH & LEARNING:**\n'
      grouped.torah.slice(0, 2).forEach((item, index) => {
        bubbeResponse += `• ${item.title}\n`
        if (item.link) {
          bubbeResponseWithLinks += `• [${item.title}](${item.link}) _(${item.source})_\n`
        } else {
          bubbeResponseWithLinks += `• ${item.title} _(${item.source})_\n`
        }
      })
      bubbeResponse += '\n'
      bubbeResponseWithLinks += '\n'
    }
    
    if (grouped.local && grouped.local.length > 0) {
      const localIntros = jewishNewsFeeds.keywords.bubbe_responses.local
      const localLine = localIntros[Math.floor(Math.random() * localIntros.length)]
      bubbeResponse += localLine + '\n'
      bubbeResponseWithLinks += localLine + '\n'
      bubbeResponse += '**LOCAL NEWS:**\n'
      bubbeResponseWithLinks += '**LOCAL NEWS:**\n'
      grouped.local.slice(0, 2).forEach((item, index) => {
        bubbeResponse += `• ${item.title}\n`
        if (item.link) {
          bubbeResponseWithLinks += `• [${item.title}](${item.link}) _(${item.source})_\n`
        } else {
          bubbeResponseWithLinks += `• ${item.title} _(${item.source})_\n`
        }
      })
      bubbeResponse += '\n'
      bubbeResponseWithLinks += '\n'
    }
    
    if (grouped.culture && grouped.culture.length > 0) {
      bubbeResponse += '**CULTURE & COMMUNITY:**\n'
      bubbeResponseWithLinks += '**CULTURE & COMMUNITY:**\n'
      grouped.culture.slice(0, 2).forEach((item, index) => {
        bubbeResponse += `• ${item.title}\n`
        if (item.link) {
          bubbeResponseWithLinks += `• [${item.title}](${item.link}) _(${item.source})_\n`
        } else {
          bubbeResponseWithLinks += `• ${item.title} _(${item.source})_\n`
        }
      })
      bubbeResponse += '\n'
      bubbeResponseWithLinks += '\n'
    }
    
    // Add Bubbe's closing
    const closings = [
      "Nu, now you're informed! Don't forget to call your mother!",
      "That's what's happening with our people. Oy, such times we live in!",
      "Stay safe, bubbeleh, and remember - we survived worse!",
      "See? This is why you need to stay connected to your people!",
      "Feh! The world is meshuga, but at least you know what's happening!"
    ]
    
    const closingLine = closings[Math.floor(Math.random() * closings.length)]
    bubbeResponse += '\n' + closingLine
    bubbeResponseWithLinks += '\n' + closingLine
    
    // Add source info footer
    bubbeResponseWithLinks += '\n\n_💡 Ask "Where is this from?" or "Show me the source" to see any article link!_'
    
    return NextResponse.json({
      success: true,
      summary: bubbeResponse,
      summaryWithLinks: bubbeResponseWithLinks,
      items: results,
      grouped,
      totalItems: results.length,
      location: location || 'global',
      topics
    })
    
  } catch (error) {
    console.error('Jewish news API error:', error)
    return NextResponse.json({
      success: false,
      summary: "Oy vey, I couldn't get the news right now. The internet is being difficult, like your cousin Harold!",
      items: [],
      error: error instanceof Error ? error.message : 'Unknown error'
    }, { status: 500 })
  }
}

export async function POST(request: Request) {
  // Allow POST requests for integration with chat
  const body = await request.json()
  const url = new URL(request.url)
  
  // Convert POST body to query params
  if (body.location) url.searchParams.set('location', body.location)
  if (body.topics) url.searchParams.set('topics', body.topics.join(','))
  if (body.quick) url.searchParams.set('quick', 'true')
  
  // Call GET handler
  return GET(new Request(url.toString()))
}