mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d01a1a94d0 | ||
|
|
bda4212249 | ||
|
|
694809fedf | ||
|
|
3cd1893b51 | ||
|
|
21415dcff3 | ||
|
|
e868cdce86 | ||
|
|
d66dc2cd93 | ||
|
|
5e0405f1ec |
29
README.md
29
README.md
@@ -1,3 +1,20 @@
|
|||||||
|
<p align="center">
|
||||||
|
|
||||||
|
<a href="https://fredy.orange-coding.net/">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/orangecoding/fredy/blob/master/doc/logo_white.png" width="400">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400">
|
||||||
|
<img alt="Jetbrains Open Source" src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
[](https://github.com/orangecoding/fredy/actions/workflows/docker.yml)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
# Fredy 🏡 – Your Self-Hosted Real Estate Finder for Germany
|
# Fredy 🏡 – Your Self-Hosted Real Estate Finder for Germany
|
||||||
|
|
||||||
Finding an apartment or house in Germany can be stressful and
|
Finding an apartment or house in Germany can be stressful and
|
||||||
@@ -11,12 +28,7 @@ With a modern architecture, Fredy provides a **clean Web UI**, removes
|
|||||||
duplicates across platforms, and stores results so you never see the
|
duplicates across platforms, and stores results so you never see the
|
||||||
same listing twice.
|
same listing twice.
|
||||||
|
|
||||||
<img src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400">
|
|
||||||
|
|
||||||

|
|
||||||
[](https://github.com/orangecoding/fredy/actions/workflows/docker.yml)
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -41,7 +53,12 @@ I maintain Fredy and other open-source projects in my free time.\
|
|||||||
If you find it useful, consider supporting the project 💙
|
If you find it useful, consider supporting the project 💙
|
||||||
|
|
||||||
Fredy is proudly backed by the **JetBrains Open Source Support Program**.
|
Fredy is proudly backed by the **JetBrains Open Source Support Program**.
|
||||||
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains" width="120"/>](https://jb.gg/OpenSourceSupport)
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://www.jetbrains.com/company/brand/img/logo_jb_dos_3.svg">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg">
|
||||||
|
<img alt="Jetbrains Open Source" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg">
|
||||||
|
</picture>
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
15
index.js
15
index.js
@@ -6,9 +6,10 @@ import * as jobStorage from './lib/services/storage/jobStorage.js';
|
|||||||
import FredyRuntime from './lib/FredyRuntime.js';
|
import FredyRuntime from './lib/FredyRuntime.js';
|
||||||
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
||||||
import './lib/api/api.js';
|
import './lib/api/api.js';
|
||||||
import { track } from './lib/services/tracking/Tracker.js';
|
|
||||||
import { handleDemoUser } from './lib/services/storage/userStorage.js';
|
import { handleDemoUser } from './lib/services/storage/userStorage.js';
|
||||||
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
||||||
|
import { initTrackerCron } from './lib/services/tracking/Tracker-Cron.js';
|
||||||
|
import logger from './lib/services/logger.js';
|
||||||
//if db folder does not exist, ensure to create it before loading anything else
|
//if db folder does not exist, ensure to create it before loading anything else
|
||||||
if (!fs.existsSync('./db')) {
|
if (!fs.existsSync('./db')) {
|
||||||
fs.mkdirSync('./db');
|
fs.mkdirSync('./db');
|
||||||
@@ -17,25 +18,23 @@ const path = './lib/provider';
|
|||||||
const provider = fs.readdirSync(path).filter((file) => file.endsWith('.js'));
|
const provider = fs.readdirSync(path).filter((file) => file.endsWith('.js'));
|
||||||
//assuming interval is always in minutes
|
//assuming interval is always in minutes
|
||||||
const INTERVAL = config.interval * 60 * 1000;
|
const INTERVAL = config.interval * 60 * 1000;
|
||||||
/* eslint-disable no-console */
|
logger.info(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
|
||||||
console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
|
|
||||||
if (config.demoMode) {
|
if (config.demoMode) {
|
||||||
console.info('Running in demo mode');
|
logger.info('Running in demo mode');
|
||||||
cleanupDemoAtMidnight();
|
cleanupDemoAtMidnight();
|
||||||
}
|
}
|
||||||
/* eslint-enable no-console */
|
|
||||||
const fetchedProvider = await Promise.all(
|
const fetchedProvider = await Promise.all(
|
||||||
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`)),
|
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
handleDemoUser();
|
handleDemoUser();
|
||||||
|
await initTrackerCron();
|
||||||
|
|
||||||
setInterval(
|
setInterval(
|
||||||
(function exec() {
|
(function exec() {
|
||||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||||
if (!config.demoMode) {
|
if (!config.demoMode) {
|
||||||
if (isDuringWorkingHoursOrNotSet) {
|
if (isDuringWorkingHoursOrNotSet) {
|
||||||
track();
|
|
||||||
config.lastRun = Date.now();
|
config.lastRun = Date.now();
|
||||||
jobStorage
|
jobStorage
|
||||||
.getJobs()
|
.getJobs()
|
||||||
@@ -51,9 +50,7 @@ setInterval(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
/* eslint-disable no-console */
|
logger.debug('Working hours set. Skipping as outside of working hours.');
|
||||||
console.debug('Working hours set. Skipping as outside of working hours.');
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exec;
|
return exec;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { setKnownListings, getKnownListings } from './services/storage/listingsS
|
|||||||
import * as notify from './notification/notify.js';
|
import * as notify from './notification/notify.js';
|
||||||
import Extractor from './services/extractor/extractor.js';
|
import Extractor from './services/extractor/extractor.js';
|
||||||
import urlModifier from './services/queryStringMutator.js';
|
import urlModifier from './services/queryStringMutator.js';
|
||||||
|
import logger from './services/logger.js';
|
||||||
|
|
||||||
class FredyRuntime {
|
class FredyRuntime {
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +60,7 @@ class FredyRuntime {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
console.error(err);
|
logger.error(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,9 +105,7 @@ class FredyRuntime {
|
|||||||
const filteredList = listings.filter((listing) => {
|
const filteredList = listings.filter((listing) => {
|
||||||
const similar = this._similarityCache.hasSimilarEntries(listing.title, listing.address);
|
const similar = this._similarityCache.hasSimilarEntries(listing.title, listing.address);
|
||||||
if (similar) {
|
if (similar) {
|
||||||
/* eslint-disable no-console */
|
logger.debug(`Filtering similar entry for title: ${listing.title} and address ${listing.address}`);
|
||||||
console.debug(`Filtering similar entry for title: ${listing.title} and address ${listing.address}`);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
}
|
||||||
return !similar;
|
return !similar;
|
||||||
});
|
});
|
||||||
@@ -115,7 +114,7 @@ class FredyRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleError(err) {
|
_handleError(err) {
|
||||||
if (err.name !== 'NoNewListingsWarning') console.error(err);
|
if (err.name !== 'NoNewListingsWarning') logger.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import files from 'serve-static';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getDirName } from '../utils.js';
|
import { getDirName } from '../utils.js';
|
||||||
import { demoRouter } from './routes/demoRouter.js';
|
import { demoRouter } from './routes/demoRouter.js';
|
||||||
|
import logger from '../services/logger.js';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||||
const PORT = config.port || 9998;
|
const PORT = config.port || 9998;
|
||||||
@@ -34,7 +35,6 @@ service.use('/api/login', loginRouter);
|
|||||||
//this route is unsecured intentionally as it is being queried from the login page
|
//this route is unsecured intentionally as it is being queried from the login page
|
||||||
service.use('/api/demo', demoRouter);
|
service.use('/api/demo', demoRouter);
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
service.start(PORT).then(() => {
|
service.start(PORT).then(() => {
|
||||||
console.info(`Started API service on port ${PORT}`);
|
logger.debug(`Started API service on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import restana from 'restana';
|
|||||||
import { config, getDirName, readConfigFromStorage, refreshConfig } from '../../utils.js';
|
import { config, getDirName, readConfigFromStorage, refreshConfig } from '../../utils.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { handleDemoUser } from '../../services/storage/userStorage.js';
|
import { handleDemoUser } from '../../services/storage/userStorage.js';
|
||||||
|
import logger from '../../services/logger.js';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const generalSettingsRouter = service.newRouter();
|
const generalSettingsRouter = service.newRouter();
|
||||||
generalSettingsRouter.get('/', async (req, res) => {
|
generalSettingsRouter.get('/', async (req, res) => {
|
||||||
@@ -20,7 +21,7 @@ generalSettingsRouter.post('/', async (req, res) => {
|
|||||||
await refreshConfig();
|
await refreshConfig();
|
||||||
handleDemoUser();
|
handleDemoUser();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
logger.error(err);
|
||||||
res.send(new Error('Error while trying to write settings.'));
|
res.send(new Error('Error while trying to write settings.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as jobStorage from '../../services/storage/jobStorage.js';
|
|||||||
import * as userStorage from '../../services/storage/userStorage.js';
|
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';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const jobRouter = service.newRouter();
|
const jobRouter = service.newRouter();
|
||||||
function doesJobBelongsToUser(job, req) {
|
function doesJobBelongsToUser(job, req) {
|
||||||
@@ -43,7 +44,7 @@ jobRouter.post('/', async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.send(new Error(error));
|
res.send(new Error(error));
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
@@ -58,7 +59,7 @@ jobRouter.delete('', async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.send(new Error(error));
|
res.send(new Error(error));
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
@@ -77,7 +78,7 @@ jobRouter.put('/:jobId/status', async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.send(new Error(error));
|
res.send(new Error(error));
|
||||||
console.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as userStorage from '../../services/storage/userStorage.js';
|
|||||||
import * as hasher from '../../services/security/hash.js';
|
import * as hasher from '../../services/security/hash.js';
|
||||||
import { config } from '../../utils.js';
|
import { config } from '../../utils.js';
|
||||||
import { trackDemoAccessed } from '../../services/tracking/Tracker.js';
|
import { trackDemoAccessed } from '../../services/tracking/Tracker.js';
|
||||||
|
import logger from '../../services/logger.js';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const loginRouter = service.newRouter();
|
const loginRouter = service.newRouter();
|
||||||
loginRouter.get('/user', async (req, res) => {
|
loginRouter.get('/user', async (req, res) => {
|
||||||
@@ -35,7 +36,7 @@ loginRouter.post('/', async (req, res) => {
|
|||||||
res.send(200);
|
res.send(200);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.error(`User ${username} tried to login, but password was wrong.`);
|
logger.error(`User ${username} tried to login, but password was wrong.`);
|
||||||
}
|
}
|
||||||
res.send(401);
|
res.send(401);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Handlebars from 'handlebars';
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { markdown2Html } from '../../services/markdown.js';
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
import { getDirName, normalizeImageUrl } from '../../utils.js';
|
import { getDirName, normalizeImageUrl } from '../../utils.js';
|
||||||
|
import logger from '../../services/logger.js';
|
||||||
|
|
||||||
const __dirname = getDirName();
|
const __dirname = getDirName();
|
||||||
const template = fs.readFileSync(path.resolve(__dirname + '/notification/emailTemplate/template.hbs'), 'utf8');
|
const template = fs.readFileSync(path.resolve(__dirname + '/notification/emailTemplate/template.hbs'), 'utf8');
|
||||||
@@ -24,7 +25,7 @@ const toBase64 = async (url) => {
|
|||||||
const ab = await res.arrayBuffer();
|
const ab = await res.arrayBuffer();
|
||||||
return Buffer.from(ab).toString('base64');
|
return Buffer.from(ab).toString('base64');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching image from ${url}:`, error.message);
|
logger.error(`Error fetching image from ${url}:`, error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -62,7 +63,7 @@ const mapListingsWithCid = async (serviceName, jobKey, listings) => {
|
|||||||
item.hasImage = true;
|
item.hasImage = true;
|
||||||
item.imageCid = cid;
|
item.imageCid = cid;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Skipping image for listing ${i} due to error: ${error.message}`);
|
logger.warn(`Skipping image for listing ${i} due to error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
import utils, { buildHash } from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
import { convertWebToMobile } from '../services/immoscout/immoscout-web-translator.js';
|
import { convertWebToMobile } from '../services/immoscout/immoscout-web-translator.js';
|
||||||
|
import logger from '../services/logger.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
async function getListings(url) {
|
async function getListings(url) {
|
||||||
@@ -52,7 +53,7 @@ async function getListings(url) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Error fetching data from ImmoScout Mobile API:', response.statusText);
|
logger.error('Error fetching data from ImmoScout Mobile API:', response.statusText);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { setInterval } from 'node:timers';
|
|||||||
import { removeJobsByUserName } from './storage/jobStorage.js';
|
import { removeJobsByUserName } from './storage/jobStorage.js';
|
||||||
import { config } from '../utils.js';
|
import { config } from '../utils.js';
|
||||||
import { getUsers } from './storage/userStorage.js';
|
import { getUsers } from './storage/userStorage.js';
|
||||||
|
import logger from './logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
|
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
|
||||||
@@ -29,7 +30,7 @@ function cleanup() {
|
|||||||
if (config.demoMode) {
|
if (config.demoMode) {
|
||||||
const demoUser = getUsers(false).find((user) => user.username === 'demo');
|
const demoUser = getUsers(false).find((user) => user.username === 'demo');
|
||||||
if (demoUser == null) {
|
if (demoUser == null) {
|
||||||
console.error('Demo user not found, cannot remove Jobs');
|
logger.error('Demo user not found, cannot remove Jobs');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeJobsByUserName(demoUser.id);
|
removeJobsByUserName(demoUser.id);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { setDebug } from './utils.js';
|
import { setDebug } from './utils.js';
|
||||||
import puppeteerExtractor from './puppeteerExtractor.js';
|
import puppeteerExtractor from './puppeteerExtractor.js';
|
||||||
import { loadParser, parse } from './parser/parser.js';
|
import { loadParser, parse } from './parser/parser.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -32,7 +33,7 @@ export default class Extractor {
|
|||||||
loadParser(this.responseText);
|
loadParser(this.responseText);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error trying to load page.', error);
|
logger.error('Error trying to load page.', error);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
import logger from '../../logger.js';
|
||||||
|
|
||||||
let $ = null;
|
let $ = null;
|
||||||
|
|
||||||
@@ -8,19 +9,19 @@ export function loadParser(text) {
|
|||||||
|
|
||||||
export function parse(crawlContainer, crawlFields, text, url) {
|
export function parse(crawlContainer, crawlFields, text, url) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
console.warn('No content found for ', url);
|
logger.warn('No content found for ', url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!crawlContainer || !crawlFields) {
|
if (!crawlContainer || !crawlFields) {
|
||||||
console.warn('Cannot parse, selector was empty for url ', url);
|
logger.warn('Cannot parse, selector was empty for url ', url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
if ($(crawlContainer).length === 0) {
|
if ($(crawlContainer).length === 0) {
|
||||||
console.warn('No elements in crawl container found for url ', url);
|
logger.warn('No elements in crawl container found for url ', url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ export function parse(crawlContainer, crawlFields, text, url) {
|
|||||||
|
|
||||||
parsedObject[key] = value || null;
|
parsedObject[key] = value || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error parsing field '${key}' with selector '${fieldSelector}':`, error);
|
logger.error(`Error parsing field '${key}' with selector '${fieldSelector}':`, error);
|
||||||
parsedObject[key] = null;
|
parsedObject[key] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,9 +67,7 @@ export function parse(crawlContainer, crawlFields, text, url) {
|
|||||||
if (parsedObject.id != null) {
|
if (parsedObject.id != null) {
|
||||||
result.push(parsedObject);
|
result.push(parsedObject);
|
||||||
} else {
|
} else {
|
||||||
/* eslint-disable no-console */
|
logger.debug('ID not found. Not relaying object.');
|
||||||
console.debug('ID not found. Not relaying object.');
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +90,7 @@ function applyModifiers(value, modifiers) {
|
|||||||
value = value.replace(/\n/g, ' ');
|
value = value.replace(/\n/g, ' ');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown modifier: ${modifier}`);
|
logger.warn(`Unknown modifier: ${modifier}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import puppeteer from 'puppeteer-extra';
|
import puppeteer from 'puppeteer-extra';
|
||||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
import { debug, DEFAULT_HEADER, botDetected } from './utils.js';
|
import { debug, DEFAULT_HEADER, botDetected } from './utils.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
puppeteer.use(StealthPlugin());
|
puppeteer.use(StealthPlugin());
|
||||||
|
|
||||||
@@ -33,13 +34,13 @@ export default async function execute(url, waitForSelector, options) {
|
|||||||
const statusCode = response.status();
|
const statusCode = response.status();
|
||||||
|
|
||||||
if (botDetected(pageSource, statusCode)) {
|
if (botDetected(pageSource, statusCode)) {
|
||||||
console.warn('We have been detected as a bot :-/ Tried url: => ', url);
|
logger.warn('We have been detected as a bot :-/ Tried url: => ', url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await page.content();
|
return await page.content();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error executing with puppeteer executor', error);
|
logger.error('Error executing with puppeteer executor', error);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (browser != null) {
|
if (browser != null) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
let debuggingOn = false;
|
let debuggingOn = false;
|
||||||
|
|
||||||
export const DEFAULT_HEADER = {
|
export const DEFAULT_HEADER = {
|
||||||
@@ -15,9 +17,7 @@ export const setDebug = (options) => {
|
|||||||
|
|
||||||
export const debug = (message) => {
|
export const debug = (message) => {
|
||||||
if (debuggingOn) {
|
if (debuggingOn) {
|
||||||
/* eslint-disable no-console */
|
logger.debug(message);
|
||||||
console.debug(message);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
61
lib/services/logger.js
Normal file
61
lib/services/logger.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const COLORS = {
|
||||||
|
debug: '\x1b[36m',
|
||||||
|
info: '\x1b[32m',
|
||||||
|
warn: '\x1b[33m',
|
||||||
|
error: '\x1b[31m',
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
};
|
||||||
|
|
||||||
|
const useColor = process.stdout.isTTY || process.stderr.isTTY;
|
||||||
|
|
||||||
|
function ts() {
|
||||||
|
const d = new Date();
|
||||||
|
const yyyy = d.getFullYear();
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0');
|
||||||
|
const mi = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lvl(level) {
|
||||||
|
const upper = level.toUpperCase();
|
||||||
|
if (!useColor) return upper;
|
||||||
|
return `${COLORS[level] || ''}${upper}${COLORS.reset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
function log(level, ...args) {
|
||||||
|
const prefix = `[${ts()}] ${lvl(level)}:`;
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
console.debug(prefix, ...args);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
console.info(prefix, ...args);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
console.warn(prefix, ...args);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(prefix, ...args);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(prefix, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
debug: (...a) => log('debug', ...a),
|
||||||
|
info: (...a) => log('info', ...a),
|
||||||
|
warn: (...a) => log('warn', ...a),
|
||||||
|
error: (...a) => log('error', ...a),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Beispiel:
|
||||||
|
// import logger from './logger.js';
|
||||||
|
// const a = 'fick';
|
||||||
|
// const b = { tr: 'lolo' };
|
||||||
|
// logger.info('hallo', a, b);
|
||||||
|
// -> In IntelliJ siehst du das Objekt wie bei console.info, plus Prefix
|
||||||
@@ -4,6 +4,7 @@ import * as listingStorage from './listingsStorage.js';
|
|||||||
import { getDirName } from '../../utils.js';
|
import { getDirName } from '../../utils.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import LowdashAdapter from './LowDashAdapter.js';
|
import LowdashAdapter from './LowDashAdapter.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
const file = path.join(getDirName(), '../', 'db/jobs.json');
|
const file = path.join(getDirName(), '../', 'db/jobs.json');
|
||||||
const adapter = new JSONFileSync(file);
|
const adapter = new JSONFileSync(file);
|
||||||
@@ -91,9 +92,7 @@ export const removeJobsByUserName = (userId) => {
|
|||||||
.value();
|
.value();
|
||||||
db.write();
|
db.write();
|
||||||
if (removedDemoJobs > 0) {
|
if (removedDemoJobs > 0) {
|
||||||
/* eslint-disable no-console */
|
logger.info(`Removed ${removedDemoJobs} demo jobs`);
|
||||||
console.log(`Removed ${removedDemoJobs} demo jobs`);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const getJobs = () => {
|
export const getJobs = () => {
|
||||||
|
|||||||
17
lib/services/tracking/Tracker-Cron.js
Normal file
17
lib/services/tracking/Tracker-Cron.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import { config, inDevMode } from '../../utils.js';
|
||||||
|
import { trackMainEvent } from './Tracker.js';
|
||||||
|
|
||||||
|
async function runTask() {
|
||||||
|
//make sure to only send tracking events if the user gave us the green light and we are not in dev mode
|
||||||
|
if (config.analyticsEnabled && !inDevMode()) {
|
||||||
|
await trackMainEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initTrackerCron() {
|
||||||
|
//run directly on start
|
||||||
|
await runTask();
|
||||||
|
// then every 6 hours
|
||||||
|
cron.schedule('0 */6 * * *', runTask);
|
||||||
|
}
|
||||||
@@ -5,16 +5,13 @@ import os from 'os';
|
|||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { packageUp } from 'package-up';
|
import { packageUp } from 'package-up';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
const deviceId = getUniqueId() || 'N/A';
|
const deviceId = getUniqueId() || 'N/A';
|
||||||
const version = await getPackageVersion();
|
const version = await getPackageVersion();
|
||||||
const FREDY_TRACKING_URL = 'https://fredy.orange-coding.net/tracking';
|
const FREDY_TRACKING_URL = 'https://fredy.orange-coding.net/tracking';
|
||||||
|
|
||||||
let cached = null;
|
export const trackMainEvent = async () => {
|
||||||
let lastSent = 0;
|
|
||||||
const SIX_HOURS = 6 * 3_600_000;
|
|
||||||
|
|
||||||
export const track = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (config.analyticsEnabled && !inDevMode()) {
|
if (config.analyticsEnabled && !inDevMode()) {
|
||||||
const activeProvider = new Set();
|
const activeProvider = new Set();
|
||||||
@@ -33,23 +30,15 @@ export const track = async () => {
|
|||||||
provider: Array.from(activeProvider),
|
provider: Array.from(activeProvider),
|
||||||
});
|
});
|
||||||
|
|
||||||
const stringify = JSON.stringify(trackingObj);
|
await fetch(`${FREDY_TRACKING_URL}/main`, {
|
||||||
const now = Date.now();
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
// send if changed OR six hours passed since last send
|
body: JSON.stringify(trackingObj),
|
||||||
if (stringify !== cached || now - lastSent >= SIX_HOURS) {
|
});
|
||||||
await fetch(`${FREDY_TRACKING_URL}/main`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: stringify,
|
|
||||||
});
|
|
||||||
cached = stringify;
|
|
||||||
lastSent = now;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error sending tracking data', error);
|
logger.warn('Error sending tracking data', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +53,7 @@ export async function trackDemoAccessed() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error sending tracking data', error);
|
logger.warn('Error sending tracking data', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +85,7 @@ async function getPackageVersion() {
|
|||||||
const json = JSON.parse(packageJson);
|
const json = JSON.parse(packageJson);
|
||||||
return json.version;
|
return json.version;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading version from package.json', error);
|
logger.error('Error reading version from package.json', error);
|
||||||
}
|
}
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { readFile } from 'fs/promises';
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { DEFAULT_CONFIG } from './defaultConfig.js';
|
import { DEFAULT_CONFIG } from './defaultConfig.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import logger from './services/logger.js';
|
||||||
|
|
||||||
const RE_GT = />/g;
|
const RE_GT = />/g;
|
||||||
const RE_WEBP = /\/format\/webp/gi;
|
const RE_WEBP = /\/format\/webp/gi;
|
||||||
@@ -74,8 +75,7 @@ export async function refreshConfig() {
|
|||||||
config.demoMode ??= false;
|
config.demoMode ??= false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
config = { ...DEFAULT_CONFIG };
|
config = { ...DEFAULT_CONFIG };
|
||||||
/* eslint-disable no-console */
|
logger.info('Error reading config file.', error);
|
||||||
console.info('Error reading config file.', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export async function refreshConfig() {
|
|||||||
*/
|
*/
|
||||||
const checkIfConfigExistsAndWriteIfNot = () => {
|
const checkIfConfigExistsAndWriteIfNot = () => {
|
||||||
if (!fs.existsSync(`${getDirName()}/../conf/config.json`)) {
|
if (!fs.existsSync(`${getDirName()}/../conf/config.json`)) {
|
||||||
console.info('Could not find config file. Will create one with default values now');
|
logger.info('Could not find config file. Will create one with default values now');
|
||||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({ ...DEFAULT_CONFIG }));
|
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({ ...DEFAULT_CONFIG }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "11.6.2",
|
"version": "11.6.4",
|
||||||
"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",
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
"lowdb": "7.0.1",
|
"lowdb": "7.0.1",
|
||||||
"markdown": "^0.5.0",
|
"markdown": "^0.5.0",
|
||||||
"nanoid": "5.1.5",
|
"nanoid": "5.1.5",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.9",
|
"node-mailjet": "6.0.9",
|
||||||
"p-throttle": "^8.0.0",
|
"p-throttle": "^8.0.0",
|
||||||
|
|||||||
@@ -5521,6 +5521,11 @@ node-abi@^3.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
|
node-cron@^4.2.1:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-4.2.1.tgz#6979be4aee4702f06322d21220df8de252c8e265"
|
||||||
|
integrity sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
|
|||||||
Reference in New Issue
Block a user