feat(route/oevw): add route for oevw.at (#20595)

* feat(route/oevw): add route for oevw.at

* chore(route/oevw): improve parameter documentation

* fix(route/oevw): decode url-encoded param

---------

Co-authored-by: Samuel Kaiser <samuel.kaiser@timetoact.at>
This commit is contained in:
sk22 2025-12-05 15:33:13 +01:00 committed by GitHub
parent 28a05d45a9
commit c78612645f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 90 additions and 0 deletions

83
lib/routes/oevw/index.ts Normal file
View File

@ -0,0 +1,83 @@
import { load } from 'cheerio';
import type { Data, DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
const FEED_LANGUAGE = 'de' as const;
const FEED_TITLE = 'ÖVW Suche' as const;
const FEED_LOGO = 'https://www.oevw.at/assetversion-035142/img/layout/touch-icon.png';
const ORIGIN_URL = 'https://www.oevw.at' as const;
const BASE_URL = `${ORIGIN_URL}/suche` as const;
const API_URL = `${BASE_URL}/filter` as const;
const csrfTokenRegex = /csrfToken\s*=\s*['"](.*?)["']/;
export const route: Route = {
name: FEED_TITLE,
example: '/oevw/%7B%22rooms%22%3A%5B%222%22%2C%223%22%5D%7D',
path: '/:json?',
maintainers: ['sk22'],
categories: ['other'],
description: `
When applying a filter on https://www.oevw.at/suche, a POST request is sent
to https://www.oevw.at/suche/filter. You can take its JSON body, URL-encode it
(\`encodeURIComponent('{...}')\`) and append it to the URL, see example URL.
for this route.`,
parameters: {
json: 'JSON request body, as sent to oevw.at/suche',
},
async handler(ctx) {
const json = JSON.parse(decodeURIComponent(ctx.req.param('json') || '{}'));
// first, get https://www.oevw.at/suche to extract csrf token and cookies
const res = await ofetch.raw(BASE_URL);
const cookies = res.headers.getSetCookie().map((setCookie: string) => setCookie.split(';')[0].trim());
const csrfToken = csrfTokenRegex.exec(res._data)![1];
const headers = {
Origin: ORIGIN_URL,
Cookie: cookies.join(';'),
'X-CSRF-Token': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
Accept: '*/*',
};
// then, fetch the filter api endpoint with all necessary headers acquired
const response = await ofetch(API_URL, { body: JSON.stringify(json), method: 'POST', headers });
const $ = load(response);
const items = $('.thumb')
.toArray()
.map((el) => {
const $el = $(el);
const subtitle = $el.find('.thumb__info').text();
const title = $el.find('.thumb__heading').text();
const badge = $el.find('.thumb__badge').text().trim();
const link = $el.find('.thumb__link a').attr('href');
const image = $el.find('.thumb__image').attr('src');
const description = $el
.find('.thumb__text__item')
.toArray()
.map((item) => $(item).text())
.join(', ');
return {
title: `${subtitle} ${title}`,
link,
description: (badge ? `${badge}, ` : '') + description,
image: `${ORIGIN_URL}/${image}`,
category: badge ? [badge] : undefined,
// no pubDate :(
} satisfies DataItem;
});
return {
title: FEED_TITLE,
language: FEED_LANGUAGE,
logo: FEED_LOGO,
allowEmpty: true,
item: items,
link: BASE_URL,
} satisfies Data;
},
};

View File

@ -0,0 +1,7 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'ÖVW',
url: 'oevw.at',
description: 'Österreichisches Volkswohnungswerk, Gemeinnützige Ges.m.b.H.',
};