From 0c46fe33d2d6b160d72ccb772d290996f38e00d6 Mon Sep 17 00:00:00 2001 From: MindCollaps Date: Thu, 22 Jan 2026 01:53:48 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=95=B5=EF=B8=8F=20More=20immoscout=20?= =?UTF-8?q?details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added more details to immoscout api - description is now populated with a lot of data from the expose using app API - You can ignore certificates, if deploying locally and using the http notification adapter - More details for the test call/example for easier testing + placeholder image + actual values + address (famous Erika Mustermans address see https://de.wikipedia.org/wiki/Mustermann) - Grater timeout for geocode since the api is sometimes slow in germany - uiElement, type boolean, now has a label as well --- lib/api/routes/notificationAdapterRouter.js | 62 +++++++++++++-- lib/notification/adapter/http.js | 24 +++++- lib/provider/immoscout.js | 75 ++++++++++++++----- .../geocoding/client/nominatimClient.js | 1 + .../NotificationAdapterMutator.jsx | 5 +- 5 files changed, 140 insertions(+), 27 deletions(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index fd34cc6f..e7abf8c7 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -34,11 +34,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 +49,7 @@ notificationAdapterRouter.post('/try', async (req, res) => { }); res.send(); } catch (Exception) { + console.error('Error during notification adapter test:', Exception); res.send(new Error(Exception)); } }); @@ -54,3 +58,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..fd691b28 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,6 +4,7 @@ */ import { markdown2Html } from '../../services/markdown.js'; +import { Agent, fetch } from 'undici'; 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,22 @@ 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.dispatcher = new Agent({ + connect: { + rejectUnauthorized: false, + }, + }); + } + + return fetch(endpointUrl, fetchOptions); }; export const config = { @@ -52,6 +64,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..fefd3d54 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -66,23 +66,63 @@ 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 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 attributes.trim() + '\n\n' + freeText.trim(); } async function isListingActive(link) { @@ -125,6 +165,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 fdc70d34..444b0e5f 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 e5b75a02..7b93adba 100644 --- a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx +++ b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx @@ -153,12 +153,15 @@ export default function NotificationAdapterMutator({ return (
{uiElement.type === 'boolean' ? ( - + { setValue(selectedAdapter, uiElement, key, checked); }} /> + {uiElement.label} + ) : ( Date: Mon, 26 Jan 2026 01:42:49 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=91=80=20Requested=20changes=20+=20so?= =?UTF-8?q?me=20extra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Req: - using logger - using node-fetch Extra: - boolean input fields will trigger the validate check, because they are set undefined at first - setting them to false if they are undefined now - added more data to the description (phone number and name of the agent) --- lib/api/routes/notificationAdapterRouter.js | 4 +++- lib/notification/adapter/http.js | 8 +++----- lib/provider/immoscout.js | 14 +++++++++++++- .../NotificationAdapterMutator.jsx | 5 ++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index e7abf8c7..789aee4d 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 './lib/services/logger.js'; + const service = restana(); const notificationAdapterRouter = service.newRouter(); const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js')); @@ -49,7 +51,7 @@ notificationAdapterRouter.post('/try', async (req, res) => { }); res.send(); } catch (Exception) { - console.error('Error during notification adapter test:', Exception); + logger.error('Error during notification adapter test:', Exception); res.send(new Error(Exception)); } }); diff --git a/lib/notification/adapter/http.js b/lib/notification/adapter/http.js index fd691b28..1376340d 100644 --- a/lib/notification/adapter/http.js +++ b/lib/notification/adapter/http.js @@ -4,7 +4,7 @@ */ import { markdown2Html } from '../../services/markdown.js'; -import { Agent, fetch } from 'undici'; +import https from 'node:https'; const mapListing = (listing) => ({ address: listing.address, @@ -43,10 +43,8 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) = }; if (selfSignedCerts === true) { - fetchOptions.dispatcher = new Agent({ - connect: { - rejectUnauthorized: false, - }, + fetchOptions.agent = new https.Agent({ + rejectUnauthorized: false, }); } diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js index fefd3d54..60bf12f3 100644 --- a/lib/provider/immoscout.js +++ b/lib/provider/immoscout.js @@ -107,6 +107,15 @@ async function pushDetails(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') @@ -122,7 +131,10 @@ function buildDescription(detailBody) { }) .join('\n\n'); - return attributes.trim() + '\n\n' + freeText.trim(); + 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) { diff --git a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx index b8e5cd58..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) { From 09a95a6fe41bb04477a92609653d35ada02605fa Mon Sep 17 00:00:00 2001 From: MindCollaps Date: Mon, 26 Jan 2026 10:51:15 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=85=20Fixed=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/routes/notificationAdapterRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/routes/notificationAdapterRouter.js b/lib/api/routes/notificationAdapterRouter.js index 789aee4d..eeb5e68b 100644 --- a/lib/api/routes/notificationAdapterRouter.js +++ b/lib/api/routes/notificationAdapterRouter.js @@ -5,7 +5,7 @@ import fs from 'fs'; import restana from 'restana'; -import logger from './lib/services/logger.js'; +import logger from '../../services/logger.js'; const service = restana(); const notificationAdapterRouter = service.newRouter();