diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index fd34cc6f..eeb5e68b 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -5,6 +5,8 @@ import fs from 'fs'; import restana from 'restana'; +import logger from '../../services/logger.js'; + const service = restana(); const notificationAdapterRouter = service.newRouter(); const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js')); @@ -34,11 +36,14 @@ notificationAdapterRouter.post('/try', async (req, res) => { serviceName: 'TestCall', newListings: [ { - price: '42 €', - title: 'This is a test listing', - address: 'some address', - size: '666 2m', - link: 'https://www.orange-coding.net', + address: 'Heidestrasse 17, 51147 Köln', + description: exampleDescription, + id: '1', + imageUrl: 'https://placehold.co/600x400/png', + price: '1.000 €', + size: '76 m²', + title: 'Stilvolle gepflegte 3-Raum-Wohnung mit gehobener Innenausstattung', + url: 'https://www.orange-coding.net', }, ], notificationConfig, @@ -46,6 +51,7 @@ notificationAdapterRouter.post('/try', async (req, res) => { }); res.send(); } catch (Exception) { + logger.error('Error during notification adapter test:', Exception); res.send(new Error(Exception)); } }); @@ -54,3 +60,51 @@ notificationAdapterRouter.get('/', async (req, res) => { res.send(); }); export { notificationAdapterRouter }; + +const exampleDescription = ` +Wohnungstyp: Etagenwohnung +Nutzfläche: 76 m² +Etage: 2 von 3 +Schlafzimmer: 1 +Badezimmer: 1 +Bezugsfrei ab: 1.4.2026 +Haustiere: Nein +Garage/Stellplatz: Tiefgarage +Anzahl Garage/Stellplatz: 1 +Kaltmiete (zzgl. Nebenkosten): 1.000 € +Preis/m²: 13,16 €/m² +Nebenkosten: 230 € +Heizkosten in Nebenkosten enthalten: Ja +Gesamtmiete: 1.230 € +Kaution: 3.000,00 +Preis pro Parkfläche: 60 € +Baujahr: 2000 +Objektzustand: Modernisiert +Qualität der Ausstattung: Gehoben +Heizungsart: Fernwärme +Energieausweistyp: Verbrauchsausweis +Energieausweis: liegt vor +Endenergieverbrauch: 72 kWh/(m²∙a) +Baujahr laut Energieausweis: 2000 + +Diese moderne 3-Zimmer-Wohnung liegt direkt neben einem Park und nur wenige Minuten von der S-Bahn-Haltestelle entfernt. Das Stadtzentrum sowie Freizeiteinrichtungen sind 1,5 km entfernt. + +Die Wohnung ist ideal für Paare oder kleine Familien geeignet. + +Ausstattung: +- neuer hochwertiger Bodenbelag (Holzoptik) in allen Räumen außer Bad/Küche +- sonniger Balkon (Süd) +- Tiefgaragenstellplatz +- Kellerabteil +- gepflegtes Mehrfamilienhaus + +Die Küche ist vom Mieter nach eigenen Wünschen einzurichten. + +Vermietung direkt vom Eigentümer - provisionsfrei! + +Lage: +• Park: 1 Minute zu Fuß +• S-Bahn Station: 2 Minuten zu Fuß +• Supermärkte, Restaurants, täglicher Bedarf in der Nähe +• Gute Anbindung Richtung Großstadt und Flughafen +`; diff --git a/lib/notification/adapter/http.js b/lib/notification/adapter/http.js index 00e23840..1376340d 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,6 +4,7 @@ */ import { markdown2Html } from '../../services/markdown.js'; +import https from 'node:https'; const mapListing = (listing) => ({ address: listing.address, @@ -17,7 +18,7 @@ const mapListing = (listing) => ({ }); export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => { - const { authToken, endpointUrl } = notificationConfig.find((a) => a.id === config.id).fields; + const { authToken, endpointUrl, selfSignedCerts } = notificationConfig.find((a) => a.id === config.id).fields; const listings = newListings.map(mapListing); const body = { @@ -34,11 +35,20 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) = headers['Authorization'] = `Bearer ${authToken}`; } - return fetch(endpointUrl, { + let fetchOptions = { method: 'POST', - headers: headers, + headers, + timeout: 10000, body: JSON.stringify(body), - }); + }; + + if (selfSignedCerts === true) { + fetchOptions.agent = new https.Agent({ + rejectUnauthorized: false, + }); + } + + return fetch(endpointUrl, fetchOptions); }; export const config = { @@ -52,6 +62,10 @@ export const config = { label: 'Endpoint URL', type: 'text', }, + selfSignedCerts: { + label: 'Self-signed certificates', + type: 'boolean', + }, authToken: { description: "Your application's auth token, if required by your endpoint.", label: 'Auth token (optional)', diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js index 6047f006..60bf12f3 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -66,23 +66,75 @@ async function getListings(url) { } const responseBody = await response.json(); - return responseBody.resultListItems - .filter((item) => item.type === 'EXPOSE_RESULT') - .map((expose) => { - const item = expose.item; - const [price, size] = item.attributes; - const image = item?.titlePicture?.preview ?? null; - return { - id: item.id, - price: price?.value, - size: size?.value, - title: item.title, - description: item.description, - link: `${metaInformation.baseUrl}expose/${item.id}`, - address: item.address?.line, - image, - }; - }); + return Promise.all( + responseBody.resultListItems + .filter((item) => item.type === 'EXPOSE_RESULT') + .map(async (expose) => { + const item = expose.item; + const [price, size] = item.attributes; + const image = item?.titlePicture?.full ?? item?.titlePicture?.preview ?? null; + let listing = { + id: item.id, + price: price?.value, + size: size?.value, + title: item.title, + link: `${metaInformation.baseUrl}expose/${item.id}`, + address: item.address?.line, + image, + }; + return await pushDetails(listing); + }), + ); +} + +async function pushDetails(listing) { + const detailed = await fetch(`https://api.mobile.immobilienscout24.de/expose/${listing.id}`, { + headers: { + 'User-Agent': 'ImmoScout_27.3_26.0_._', + 'Content-Type': 'application/json', + }, + }); + if (!detailed.ok) { + logger.error('Error fetching listing details from ImmoScout Mobile API:', detailed.statusText); + return ''; + } + const detailBody = await detailed.json(); + + listing.description = buildDescription(detailBody); + + return listing; +} + +function buildDescription(detailBody) { + const sections = detailBody.sections || []; + const contact = detailBody.contact || {}; + const cData = contact?.contactData || {}; + const agentName = cData?.agent?.name || ''; + const agentCompany = cData?.agent?.company || ''; + const stars = cData?.agent?.rating?.numberOfStars || ''; + const phoneNumbers = contact?.phoneNumbers || []; + const phoneNumbersMapped = phoneNumbers + .map((p) => `${p.label}: ${p.text}`) + .join('\n').trim(); + + const attributes = sections + .filter((s) => s.type === 'ATTRIBUTE_LIST') + .flatMap((s) => s.attributes) + .filter((attr) => attr.label && attr.text) + .map((attr) => `${attr.label} ${attr.text}`) + .join('\n'); + + const freeText = sections + .filter((s) => s.type === 'TEXT_AREA') + .map((s) => { + return `${s.title}\n${s.text}`; + }) + .join('\n\n'); + + return `Agent: ${agentName ? agentName : 'Unbekannt'} ${agentCompany ? `(${agentCompany}) ` : ''}${stars ? `- ${stars} stars` : ''}\n` + + (phoneNumbersMapped ? `Phone Numbers:\n${phoneNumbersMapped}` : '') + '\n\n' + + attributes.trim() + '\n\n' + + freeText.trim(); } async function isListingActive(link) { @@ -125,6 +177,7 @@ const config = { size: 'size', link: 'link', address: 'address', + description: 'description', }, // Not required - used by filter to remove and listings that failed to parse sortByDateParam: 'sorting=-firstactivation', diff --git a/lib/services/geocoding/client/nominatimClient.js b/lib/services/geocoding/client/nominatimClient.js index be284546..d2cd1e05 100644 --- a/lib/services/geocoding/client/nominatimClient.js +++ b/lib/services/geocoding/client/nominatimClient.js @@ -67,6 +67,7 @@ async function doGeocode(address) { try { const response = await fetch(url, { agent, + timeout: 60000, headers: { 'User-Agent': userAgent, }, diff --git a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx index 2ff301d6..6e0e5963 100644 --- a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx +++ b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx @@ -27,10 +27,13 @@ const sortAdapter = (a, b) => { const validate = (selectedAdapter) => { const results = []; for (let uiElement of Object.values(selectedAdapter.fields || [])) { - if (uiElement.value == null && !uiElement.optional) { + if (uiElement.value == null && !uiElement.optional && uiElement.type !== 'boolean') { results.push('All fields are mandatory and must be set.'); continue; } + if (uiElement.type === 'boolean' && typeof uiElement.value !== 'boolean') { + uiElement.value = false; + } if (uiElement.type === 'number') { const numberValue = parseFloat(uiElement.value); if (isNaN(numberValue) || numberValue < 0) { @@ -153,12 +156,15 @@ export default function NotificationAdapterMutator({ return (
{uiElement.type === 'boolean' ? ( - + { setValue(selectedAdapter, uiElement, key, checked); }} /> + {uiElement.label} + ) : (