mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da8fd13973 | ||
|
|
7deffc64af | ||
|
|
d1dad7fd3b |
56
index.js
56
index.js
@@ -10,6 +10,7 @@ import { ensureDemoUserExists, ensureAdminUserExists } from './lib/services/stor
|
|||||||
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
||||||
import { initTrackerCron } from './lib/services/tracking/Tracker-Cron.js';
|
import { initTrackerCron } from './lib/services/tracking/Tracker-Cron.js';
|
||||||
import logger from './lib/services/logger.js';
|
import logger from './lib/services/logger.js';
|
||||||
|
import { bus } from './lib/services/events/event-bus.js';
|
||||||
|
|
||||||
// Ensure sqlite directory exists before loading anything else (based on config.sqlitepath)
|
// Ensure sqlite directory exists before loading anything else (based on config.sqlitepath)
|
||||||
const rawDir = config.sqlitepath || '/db';
|
const rawDir = config.sqlitepath || '/db';
|
||||||
@@ -45,29 +46,34 @@ ensureAdminUserExists();
|
|||||||
ensureDemoUserExists();
|
ensureDemoUserExists();
|
||||||
await initTrackerCron();
|
await initTrackerCron();
|
||||||
|
|
||||||
setInterval(
|
bus.on('jobs:runAll', () => {
|
||||||
(function exec() {
|
logger.debug('Running Fredy Job manually');
|
||||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
execute();
|
||||||
if (!config.demoMode) {
|
});
|
||||||
if (isDuringWorkingHoursOrNotSet) {
|
|
||||||
config.lastRun = Date.now();
|
const execute = () => {
|
||||||
jobStorage
|
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||||
.getJobs()
|
if (!config.demoMode) {
|
||||||
.filter((job) => job.enabled)
|
if (isDuringWorkingHoursOrNotSet) {
|
||||||
.forEach((job) => {
|
config.lastRun = Date.now();
|
||||||
job.provider
|
jobStorage
|
||||||
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
.getJobs()
|
||||||
.forEach(async (prov) => {
|
.filter((job) => job.enabled)
|
||||||
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
.forEach((job) => {
|
||||||
pro.init(prov, job.blacklist);
|
job.provider
|
||||||
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
||||||
});
|
.forEach(async (prov) => {
|
||||||
});
|
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
||||||
} else {
|
pro.init(prov, job.blacklist);
|
||||||
logger.debug('Working hours set. Skipping as outside of working hours.');
|
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.debug('Working hours set. Skipping as outside of working hours.');
|
||||||
}
|
}
|
||||||
return exec;
|
}
|
||||||
})(),
|
};
|
||||||
INTERVAL,
|
|
||||||
);
|
setInterval(execute, INTERVAL);
|
||||||
|
//start once at startup
|
||||||
|
execute();
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import * as userStorage from '../../services/storage/userStorage.js';
|
|||||||
import { config } from '../../utils.js';
|
import { config } from '../../utils.js';
|
||||||
import { isAdmin } from '../security.js';
|
import { isAdmin } from '../security.js';
|
||||||
import logger from '../../services/logger.js';
|
import logger from '../../services/logger.js';
|
||||||
|
import { bus } from '../../services/events/event-bus.js';
|
||||||
|
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const jobRouter = service.newRouter();
|
const jobRouter = service.newRouter();
|
||||||
|
|
||||||
function doesJobBelongsToUser(job, req) {
|
function doesJobBelongsToUser(job, req) {
|
||||||
const userId = req.session.currentUser;
|
const userId = req.session.currentUser;
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
@@ -17,6 +20,7 @@ function doesJobBelongsToUser(job, req) {
|
|||||||
}
|
}
|
||||||
return user.isAdmin || job.userId === user.id;
|
return user.isAdmin || job.userId === user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
jobRouter.get('/', async (req, res) => {
|
jobRouter.get('/', async (req, res) => {
|
||||||
const isUserAdmin = isAdmin(req);
|
const isUserAdmin = isAdmin(req);
|
||||||
//show only the jobs which belongs to the user (or all of the user is an admin)
|
//show only the jobs which belongs to the user (or all of the user is an admin)
|
||||||
@@ -30,6 +34,12 @@ jobRouter.get('/processingTimes', async (req, res) => {
|
|||||||
};
|
};
|
||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jobRouter.post('/startAll', async (req, res) => {
|
||||||
|
bus.emit('jobs:runAll');
|
||||||
|
res.send();
|
||||||
|
});
|
||||||
|
|
||||||
jobRouter.post('/', async (req, res) => {
|
jobRouter.post('/', async (req, res) => {
|
||||||
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled } = req.body;
|
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled } = req.body;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* The mobile API provides the following endpoints:
|
* The mobile API provides the following endpoints:
|
||||||
* - GET /search/total?{search parameters}: Returns the total number of listings for the given query
|
* - GET /search/total?{search parameters}: Returns the total number of listings for the given query
|
||||||
* Example: `curl -H "User-Agent: ImmoScout24_1410_30_._" https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin `
|
* Example: `curl -H "User-Agent: ImmoScout_27.3_26.0_._" https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin `
|
||||||
*
|
*
|
||||||
* - POST /search/list?{search parameters}: Actually retrieves the listings. Body is json encoded and contains
|
* - POST /search/list?{search parameters}: Actually retrieves the listings. Body is json encoded and contains
|
||||||
* data specifying additional results (advertisements) to return. The format is as follows:
|
* data specifying additional results (advertisements) to return. The format is as follows:
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
* ```
|
* ```
|
||||||
* It is not necessary to provide data for the specified keys.
|
* It is not necessary to provide data for the specified keys.
|
||||||
*
|
*
|
||||||
* Example: `curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' -H "Connection: keep-alive" -H "User-Agent: ImmoScout24_1410_30_._" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"supportedResultListType": [], "userData": {}}'`
|
* Example: `curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' -H "Connection: keep-alive" -H "User-Agent: ImmoScout_27.3_26.0_._" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"supportedResultListType": [], "userData": {}}'`
|
||||||
|
|
||||||
* - GET /expose/{id} - Returns the details of a listing. The response contains additional details not included in the
|
* - GET /expose/{id} - Returns the details of a listing. The response contains additional details not included in the
|
||||||
* listing response.
|
* listing response.
|
||||||
*
|
*
|
||||||
* Example: `curl -H "User-Agent: ImmoScout24_1410_30_._" "https://api.mobile.immobilienscout24.de/expose/158382494"`
|
* Example: `curl -H "User-Agent: ImmoScout_27.3_26.0_._" "https://api.mobile.immobilienscout24.de/expose/158382494"`
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* It is necessary to set the correct User Agent (see `getListings`) in the request header.
|
* It is necessary to set the correct User Agent (see `getListings`) in the request header.
|
||||||
@@ -44,7 +44,7 @@ async function getListings(url) {
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'ImmoScout24_1410_30_._',
|
'User-Agent': 'ImmoScout_27.3_26.0_._',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
2
lib/services/events/event-bus.js
Normal file
2
lib/services/events/event-bus.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
export const bus = new EventEmitter();
|
||||||
@@ -21,7 +21,7 @@ export function parse(crawlContainer, crawlFields, text, url) {
|
|||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
if ($(crawlContainer).length === 0) {
|
if ($(crawlContainer).length === 0) {
|
||||||
logger.warn('No elements in crawl container found for url ', url);
|
logger.debug('No elements in crawl container found for url ', url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ export const storeListings = (jobId, providerId, listings) => {
|
|||||||
|
|
||||||
SqliteConnection.withTransaction((db) => {
|
SqliteConnection.withTransaction((db) => {
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
`INSERT INTO listings (id, hash, provider, job_id, price, size, title, image_url, description, address, city,
|
`INSERT INTO listings (id, hash, provider, job_id, price, size, title, image_url, description, address,
|
||||||
link, created_at)
|
link, created_at)
|
||||||
VALUES (@id, @hash, @provider, @job_id, @price, @size, @title, @image_url, @description, @address, @city, @link,
|
VALUES (@id, @hash, @provider, @job_id, @price, @size, @title, @image_url, @description, @address, @link,
|
||||||
@created_at)
|
@created_at)
|
||||||
ON CONFLICT(hash) DO NOTHING`,
|
ON CONFLICT(job_id, hash) DO NOTHING`,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const item of listings) {
|
for (const item of listings) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Migration: there needs to be a unique index on job_id and hash as only
|
||||||
|
// this makes the listing indeed unique
|
||||||
|
|
||||||
|
export function up(db) {
|
||||||
|
db.exec(`
|
||||||
|
DROP INDEX IF EXISTS idx_listings_hash;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_listings_job_hash
|
||||||
|
ON listings (job_id, hash);
|
||||||
|
`);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "12.1.0",
|
"version": "12.1.2",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@douyinfe/semi-icons": "^2.86.0",
|
||||||
"@douyinfe/semi-ui": "2.86.0",
|
"@douyinfe/semi-ui": "2.86.0",
|
||||||
"@sendgrid/mail": "8.1.5",
|
"@sendgrid/mail": "8.1.5",
|
||||||
"@visactor/react-vchart": "^2.0.4",
|
"@visactor/react-vchart": "^2.0.4",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Challenges:
|
|||||||
_Returns the total number of listings for the given query._
|
_Returns the total number of listings for the given query._
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "User-Agent: ImmoScout24_1410_30_._" \
|
curl -H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin"
|
"https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin"
|
||||||
```
|
```
|
||||||
@@ -63,7 +63,7 @@ _The body is json encoded and contains data specifying additional results (adver
|
|||||||
```
|
```
|
||||||
curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' \
|
curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' \
|
||||||
-H "Connection: keep-alive" \
|
-H "Connection: keep-alive" \
|
||||||
-H "User-Agent: ImmoScout24_1410_30_._" \
|
-H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"supportedResultListType":[],"userData":{}}'
|
-d '{"supportedResultListType":[],"userData":{}}'
|
||||||
@@ -78,7 +78,7 @@ curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calc
|
|||||||
The response contains additional details not included in the listing response.
|
The response contains additional details not included in the listing response.
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "User-Agent: ImmoScout24_1410_30_._" \
|
curl -H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"https://api.mobile.immobilienscout24.de/expose/158382494"
|
"https://api.mobile.immobilienscout24.de/expose/158382494"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('#immoscout-mobile URL conversion', () => {
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'ImmoScout24_1410_30_._',
|
'User-Agent': 'ImmoScout_27.3_26.0_._',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { format } from '../../services/time/timeService';
|
import { format } from '../../services/time/timeService';
|
||||||
import { Descriptions } from '@douyinfe/semi-ui';
|
import { Button, Descriptions, Toast } from '@douyinfe/semi-ui';
|
||||||
|
import { IconPlayCircle } from '@douyinfe/semi-icons';
|
||||||
|
import { xhrPost } from '../../services/xhr.js';
|
||||||
|
|
||||||
export default function ProcessingTimes({ processingTimes = {} }) {
|
export default function ProcessingTimes({ processingTimes = {} }) {
|
||||||
if (Object.keys(processingTimes).length === 0) {
|
if (Object.keys(processingTimes).length === 0) {
|
||||||
@@ -24,6 +26,19 @@ export default function ProcessingTimes({ processingTimes = {} }) {
|
|||||||
<Descriptions.Item itemKey="Next run">
|
<Descriptions.Item itemKey="Next run">
|
||||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey="Find Listings now">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<IconPlayCircle />}
|
||||||
|
aria-label="Start now"
|
||||||
|
onClick={async () => {
|
||||||
|
await xhrPost('/api/jobs/startAll', null);
|
||||||
|
Toast.success('Successfully triggered Fredy search.');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Search now
|
||||||
|
</Button>
|
||||||
|
</Descriptions.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|||||||
@@ -973,7 +973,7 @@
|
|||||||
remark-gfm "^4.0.0"
|
remark-gfm "^4.0.0"
|
||||||
scroll-into-view-if-needed "^2.2.24"
|
scroll-into-view-if-needed "^2.2.24"
|
||||||
|
|
||||||
"@douyinfe/semi-icons@2.86.0":
|
"@douyinfe/semi-icons@2.86.0", "@douyinfe/semi-icons@^2.86.0":
|
||||||
version "2.86.0"
|
version "2.86.0"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.86.0.tgz#ee4355c81616ea4325627a3bb607ed9f9b9afac3"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.86.0.tgz#ee4355c81616ea4325627a3bb607ed9f9b9afac3"
|
||||||
integrity sha512-KEDlYYP1wdOqN28Ck0YcdCx7mSks8SRY4w4KKbXPaROzYNEyT2BRcJxwysMHfxL2IDfsroHrRPJsX9pnrmQqTg==
|
integrity sha512-KEDlYYP1wdOqN28Ck0YcdCx7mSks8SRY4w4KKbXPaROzYNEyT2BRcJxwysMHfxL2IDfsroHrRPJsX9pnrmQqTg==
|
||||||
|
|||||||
Reference in New Issue
Block a user