const http = require('http'); const https = require('https'); const url = require('url'); const PORT = 3002; // Default configuration let config = { sites: [ { id: '1411', name: 'Ambassaderna', enabled: true } ], rssFeeds: [ { name: "Travel Alerts", url: "https://travel.state.gov/content/travel/en/rss/rss.xml", enabled: true } ] }; // Function to load configuration from file function loadSitesConfig() { try { const fs = require('fs'); if (fs.existsSync('sites-config.json')) { const configData = fs.readFileSync('sites-config.json', 'utf8'); const loadedConfig = JSON.parse(configData); // Handle old format (array of sites) if (Array.isArray(loadedConfig)) { config.sites = loadedConfig; } else { config = loadedConfig; } console.log('Loaded configuration:', config); } } catch (error) { console.error('Error loading configuration:', error); } } // Load configuration on startup loadSitesConfig(); // Feed templates for different formats const feedTemplates = { rss2: { detect: (data) => data.includes(' data.includes(' data.includes('/gs, '$1'); // Remove HTML tags content = content.replace(/<[^>]+>/g, ' '); // Convert common HTML entities content = content .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec)) .replace(/&#x([A-Fa-f0-9]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))); // Remove extra whitespace content = content.replace(/\s+/g, ' ').trim(); // Get first sentence if content is too long if (content.length > 200) { const match = content.match(/^.*?[.!?](?:\s|$)/); if (match) { content = match[0].trim(); } else { content = content.substring(0, 197) + '...'; } } return content; } // Function to extract content based on template function extractContent(content, template) { const itemRegex = new RegExp(`<${template.itemPath}>([\\\s\\\S]*?)<\/${template.itemPath}>`, 'g'); const titleRegex = new RegExp(`<${template.titlePath}>([\\\s\\\S]*?)<\/${template.titlePath}>`); const descRegex = new RegExp(`<${template.descPath}>([\\\s\\\S]*?)<\/${template.descPath}>`); const dateRegex = new RegExp(`<${template.datePath}>([\\\s\\\S]*?)<\/${template.datePath}>`); const linkRegex = /]*?>([\s\S]*?)<\/link>/; const items = []; let match; while ((match = itemRegex.exec(content)) !== null) { const itemContent = match[1]; const titleMatch = titleRegex.exec(itemContent); const descMatch = descRegex.exec(itemContent); const dateMatch = dateRegex.exec(itemContent); const linkMatch = linkRegex.exec(itemContent); const title = cleanHtmlContent(titleMatch ? titleMatch[1] : ''); const description = cleanHtmlContent(descMatch ? descMatch[1] : ''); // Create display text from title and optionally description let displayText = title; if (description && description !== title) { displayText = `${title}: ${description}`; } items.push({ title, displayText, link: linkMatch ? linkMatch[1].trim() : '', description, pubDate: dateMatch ? dateMatch[1] : '', feedUrl: null // Will be set by caller }); } return items; } // Function to fetch RSS feed data from a single URL async function fetchSingleRssFeed(feedUrl) { return new Promise((resolve, reject) => { console.log(`Fetching RSS feed from: ${feedUrl}`); const request = https.get(feedUrl, (res) => { console.log(`RSS feed response status: ${res.statusCode}`); console.log('RSS feed response headers:', res.headers); let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { // Log first part of response console.log('RSS feed raw response (first 500 chars):', data.substring(0, 500)); // Detect feed type and get appropriate template const feedType = detectFeedType(data); const template = feedTemplates[feedType]; console.log(`Detected feed type: ${feedType}`); // Extract and process items const items = extractContent(data, template); console.log(`Extracted ${items.length} items from feed`); // Add feed URL to each item items.forEach(item => { item.feedUrl = feedUrl; }); resolve(items); } catch (error) { console.error('Error processing RSS feed:', error); console.error('Error stack:', error.stack); resolve([ { title: 'Error processing RSS feed', link: feedUrl, feedUrl: feedUrl, error: error.message } ]); } }); }); request.on('error', (error) => { console.error('Error fetching RSS feed:', error); console.error('Error stack:', error.stack); resolve([ { title: 'Error fetching RSS feed', link: feedUrl, feedUrl: feedUrl, error: error.message } ]); }); // Set a timeout of 10 seconds request.setTimeout(10000, () => { console.error('RSS feed request timed out:', feedUrl); request.destroy(); resolve([ { title: 'RSS feed request timed out', link: feedUrl, feedUrl: feedUrl, error: 'Request timed out after 10 seconds' } ]); }); }); } // Function to fetch all enabled RSS feeds async function fetchRssFeed() { try { const enabledFeeds = config.rssFeeds.filter(feed => feed.enabled); const feedPromises = enabledFeeds.map(feed => fetchSingleRssFeed(feed.url)); const allItems = await Promise.all(feedPromises); // Flatten array and sort by date (if available) const items = allItems.flat().sort((a, b) => { const dateA = new Date(a.pubDate || 0); const dateB = new Date(b.pubDate || 0); return dateB - dateA; }); return { items }; } catch (error) { console.error('Error fetching RSS feeds:', error); return { items: [{ title: 'Error fetching RSS feeds', link: '#', error: error.message }] }; } } // Function to fetch data from the SL Transport API for a specific site function fetchDeparturesForSite(siteId) { return new Promise((resolve, reject) => { const apiUrl = `https://transport.integration.sl.se/v1/sites/${siteId}/departures`; console.log(`Fetching data from: ${apiUrl}`); https.get(apiUrl, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log('Raw API response:', data.substring(0, 200) + '...'); try { try { const parsedData = JSON.parse(data); console.log('Successfully parsed as regular JSON'); resolve(parsedData); return; } catch (jsonError) { console.log('Not valid JSON, trying to fix format...'); } if (data.startsWith('departures":')) { data = '{' + data; } else if (data.includes('departures":')) { const startIndex = data.indexOf('departures":'); if (startIndex > 0) { data = '{' + data.substring(startIndex); } } data = data.replace(/}{\s*"/g, '},{"'); data = data.replace(/"([^"]+)":\s*([^,{}\[\]]+)(?=")/g, '"$1": $2,'); data = data.replace(/,\s*}/g, '}').replace(/,\s*\]/g, ']'); try { const parsedData = JSON.parse(data); console.log('Successfully parsed fixed JSON'); if (parsedData && parsedData.departures) { const line7Departures = parsedData.departures.filter(d => d.line && d.line.designation === '7'); if (line7Departures.length > 0) { console.log('Line 7 details:', JSON.stringify(line7Departures[0], null, 2)); } } resolve(parsedData); } catch (parseError) { console.error('Error parsing fixed JSON:', parseError); resolve({ departures: [], error: 'Failed to parse API response: ' + parseError.message, rawResponse: data.substring(0, 500) + '...' }); } } catch (error) { console.error('Error processing API response:', error); resolve({ departures: [], error: 'Error processing API response: ' + error.message, rawResponse: data.substring(0, 500) + '...' }); } }); }).on('error', (error) => { console.error('Error fetching data from API:', error); resolve({ departures: [], error: 'Error fetching data from API: ' + error.message }); }); }); } // Create HTTP server const server = http.createServer(async (req, res) => { const parsedUrl = url.parse(req.url, true); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Function to fetch data from all enabled sites async function fetchAllDepartures() { const enabledSites = config.sites.filter(site => site.enabled); if (enabledSites.length === 0) { return { sites: [], error: 'No enabled sites configured' }; } try { const sitesPromises = enabledSites.map(async (site) => { try { const departureData = await fetchDeparturesForSite(site.id); return { siteId: site.id, siteName: site.name, data: departureData }; } catch (error) { console.error(`Error fetching departures for site ${site.id}:`, error); return { siteId: site.id, siteName: site.name, error: error.message }; } }); const results = await Promise.all(sitesPromises); return { sites: results }; } catch (error) { console.error('Error fetching all departures:', error); return { sites: [], error: error.message }; } } // Handle API endpoints if (parsedUrl.pathname === '/api/departures') { try { const data = await fetchAllDepartures(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } catch (error) { console.error('Error handling departures request:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } else if (parsedUrl.pathname === '/api/config') { // Return the current configuration res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(config)); } else if (parsedUrl.pathname === '/api/config/update' && req.method === 'POST') { // Update configuration let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { const newConfig = JSON.parse(body); if (newConfig.sites && newConfig.rssFeeds) { config = newConfig; // Save to file const fs = require('fs'); fs.writeFileSync('sites-config.json', JSON.stringify(config, null, 2)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Configuration updated' })); } else { throw new Error('Invalid configuration format'); } } catch (error) { console.error('Error updating configuration:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } }); } else if (parsedUrl.pathname === '/api/rss') { try { // If URL is provided, fetch single feed, otherwise fetch all enabled feeds const feedUrl = parsedUrl.query.url; console.log('RSS request received for URL:', feedUrl); let data; if (feedUrl) { console.log('Fetching single RSS feed:', feedUrl); const items = await fetchSingleRssFeed(feedUrl); data = { items }; console.log(`Fetched ${items.length} items from feed`); } else { console.log('Fetching all enabled RSS feeds'); data = await fetchRssFeed(); console.log(`Fetched ${data.items.length} total items from all feeds`); } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); console.log('RSS response sent successfully'); } catch (error) { console.error('Error handling RSS request:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message, stack: error.stack })); } } // Serve static files else if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/index.html') { const fs = require('fs'); fs.readFile('index.html', (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error loading index.html'); return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data); }); } else if (/\.(js|css|png|jpg|jpeg|gif|ico)$/.test(parsedUrl.pathname)) { const fs = require('fs'); const filePath = parsedUrl.pathname.substring(1); fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('File not found'); return; } const ext = parsedUrl.pathname.split('.').pop(); const contentType = { 'js': 'text/javascript', 'css': 'text/css', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'ico': 'image/x-icon' }[ext] || 'text/plain'; res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); // Start the server server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}/`); });