← 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()))
}