Compare commits

...

17 Commits

Author SHA1 Message Date
weakmap@gmail.com
395199a4a2 fixing duplicate provider removal / ugrade dependencies 2025-07-19 20:10:19 +02:00
weakmap@gmail.com
c2680fe49f next release version 2025-06-14 19:26:17 +02:00
weakmap@gmail.com
2b862b2d98 fixing blacklist 2025-06-14 19:25:52 +02:00
weakmap@gmail.com
9065448b6b upgrade dependencies 2025-06-14 19:12:55 +02:00
weakmap@gmail.com
b9f49cb5b2 upgrade dependencies 2025-06-14 19:06:27 +02:00
weakmap@gmail.com
53121742c2 improving error message 2025-06-14 19:03:23 +02:00
Christian Kellner
1a3eae0390 next version 2025-06-04 09:47:42 +02:00
Christian Kellner
a42905d63f fixing docker ignore issue 2025-06-04 09:46:07 +02:00
Christian Kellner
9917491728 Merge branch 'master' of github.com:orangecoding/fredy 2025-06-04 09:29:50 +02:00
Christian Kellner
f032e6a724 test: verify unrelated text yields no similarity (#130) 2025-06-04 09:15:53 +02:00
Christian Kellner
111c154ae3 Fix job ownership verification (#132) 2025-06-04 09:15:36 +02:00
Christian Kellner
2194ffe0f4 Fix typo in README (#133) 2025-06-04 09:15:15 +02:00
Christian Kellner
cfa25fc0e0 docs: fix adapter sentence (#131) 2025-06-04 09:14:57 +02:00
Christian Kellner
d50dd61f3e Merge branch 'master' of github.com:orangecoding/fredy 2025-06-04 09:12:00 +02:00
Christian Kellner
31e7f77bde uprade restana & vite 2025-05-27 12:01:26 +02:00
Christian Kellner
a418d64f1a uprade dependencies 2025-05-27 11:51:57 +02:00
Christian Kellner
d099872950 Update README.md 2025-05-26 13:23:36 +02:00
13 changed files with 838 additions and 632 deletions

View File

@@ -2,5 +2,6 @@ node_modules/
npm-debug.log
test/
db/
conf/
.git/
.github/

View File

@@ -8,8 +8,6 @@ _Fredy_ scrapes multiple services (Immonet, Immowelt etc.) and send new listings
If _Fredy_ finds matching results, it will send them to you via Slack, Email, Telegram etc. (More adapters can be configured.) As _Fredy_ stores the listings it has found, new results will not be sent to you twice (and as a side-effect, _Fredy_ can show some statistics). Furthermore, _Fredy_ checks duplicates per scraping so that the same listings are not being sent twice or more when posted on various platforms (which happens more often than one might think).
<a href="https://www.producthunt.com/posts/fredy-find-real-estates-damn-easy?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-fredy&#0045;find&#0045;real&#0045;estates&#0045;damn&#0045;easy" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=965690&theme=light&t=1747292331626" alt="Fredy&#0032;&#0045;&#0032;Find&#0032;Real&#0032;Estates&#0032;Damn&#0032;EasY&#0032; - Your&#0032;personal&#0032;real&#0032;estate&#0032;search&#0032;bot | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# Sponsorship [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/orangecoding)
If you like my work, consider becoming a sponsor. I'm not expecting anybody to pay for _Fredy_ or any other Open Source Project I'm maintaining, however keep in mind, I'm doing all of this in my spare time :) Thanks.
@@ -48,7 +46,7 @@ A provider contains the URL that points to the search results for the respective
**It is important that you order the search results by date, so that _Fredy_ always picks the latest results first!**
#### Adapter
_Fredy_ supports multiple adapters, such as Slack, SendGrid, Telegram etc. A search job can have as many adapters as supported by _Fredy_. Each adapter needs different configuration values, which you have to provide when using them. A adapter dictactes how the frontend renders by telling the frontend what information it needs in order to send listings to the user.
_Fredy_ supports multiple adapters, such as Slack, SendGrid, Telegram etc. A search job can have as many adapters as supported by _Fredy_. Each adapter needs different configuration values, which you have to provide when using them. An adapter dictates how the frontend renders by telling the frontend what information it needs in order to send listings to the user.
#### Jobs
A Job wraps adapters and providers. _Fredy_ runs the configured jobs in a specific interval (can be configured in `/conf/config.json`).
@@ -84,7 +82,7 @@ yarn run test
![Architecture](/doc/architecture.jpg "Architecture")
### Immoscout
Immoscout has implemented advanced bot detection. In order to work around this, we are using a reversed engineered version of their mobile api. See See [Immoscout Reverse Engineering Documentation](https://github.com/orangecoding/fredy/blob/master/reverse-engineered-immoscout.md)
Immoscout has implemented advanced bot detection. In order to work around this, we are using a reversed engineered version of their mobile api. See [Immoscout Reverse Engineering Documentation](https://github.com/orangecoding/fredy/blob/master/reverse-engineered-immoscout.md)
# Analytics
Fredy is completely free (and will always remain free). However, it would be a huge help if youd allow me to collect some analytical data.

View File

@@ -15,7 +15,7 @@ function doesJobBelongsToUser(job, req) {
if (user == null) {
return false;
}
return user.isAdmin || job.userId === job.userId;
return user.isAdmin || job.userId === user.id;
}
jobRouter.get('/', async (req, res) => {
const isUserAdmin = isAdmin(req);

View File

@@ -8,7 +8,7 @@ export function loadParser(text) {
export function parse(crawlContainer, crawlFields, text, url) {
if (!text) {
console.warn('Cannot parse, text was empty for url ', url);
console.warn('No content found for ', url);
return null;
}

View File

@@ -1,5 +1,5 @@
import { JSONFileSync } from 'lowdb/node';
import {config, getDirName} from '../../utils.js';
import { config, getDirName } from '../../utils.js';
import * as hasher from '../security/hash.js';
import { nanoid } from 'nanoid';
import * as jobStorage from './jobStorage.js';
@@ -7,23 +7,23 @@ import path from 'path';
import LowdashAdapter from './LowDashAdapter.js';
const defaultData = {
user: [
//you probably want to change the default password ;)
{
id: nanoid(),
lastLogin: Date.now(),
username: 'admin',
password: hasher.hash('admin'),
isAdmin: true,
},
{
id: nanoid(),
lastLogin: Date.now(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
},
],
user: [
//you probably want to change the default password ;)
{
id: nanoid(),
lastLogin: Date.now(),
username: 'admin',
password: hasher.hash('admin'),
isAdmin: true,
},
{
id: nanoid(),
lastLogin: Date.now(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
},
],
};
const file = path.join(getDirName(), '../', 'db/users.json');
@@ -86,34 +86,38 @@ export const removeUser = (userId) => {
db.chain
.set(
'user',
user.filter((u) => u.id !== userId)
user.filter((u) => u.id !== userId),
)
.value();
db.write();
};
export const handleDemoUser = () => {
if(!config.demoMode){
const user = db.chain.get('user').value();
db.chain.get('user').value();
db.chain.set('user', user.filter((u) => u.username !== 'demo')).value();
db.write();
}else {
const demoUser = db.chain
.get('user')
.filter((u) => u.username === 'demo')
.value();
if (demoUser == null || demoUser.length === 0) {
db.chain.get('user')
.value()
.push({
id: nanoid(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
});
db.write();
}
if (!config.demoMode) {
const user = db.chain.get('user').value();
db.chain
.set(
'user',
user.filter((u) => u.username !== 'demo'),
)
.value();
db.write();
} else {
const demoUser = db.chain
.get('user')
.filter((u) => u.username === 'demo')
.value();
if (demoUser == null || demoUser.length === 0) {
db.chain
.get('user')
.value()
.push({
id: nanoid(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
});
db.write();
}
}
};

View File

@@ -9,12 +9,9 @@ function inDevMode(){
}
function isOneOf(word, arr) {
if (arr == null || arr.length === 0) {
return false;
}
const expression = String.raw`\b(${arr.join('|')})\b`;
const blacklist = new RegExp(expression, 'ig');
return blacklist.test(word);
if (!arr || arr.length === 0 || word == null) return false;
const lowerWord = word.toLowerCase();
return arr.some(item => lowerWord.indexOf(item.toLowerCase()) !== -1);
}
function nullOrEmpty(val) {

View File

@@ -1,6 +1,6 @@
{
"name": "fredy",
"version": "11.2.2",
"version": "11.2.5",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"start": "node prod.js",
@@ -50,17 +50,17 @@
"Firefox ESR"
],
"dependencies": {
"@douyinfe/semi-ui": "2.79.0",
"@douyinfe/semi-ui": "2.83.0",
"@rematch/core": "2.2.0",
"@rematch/loading": "2.1.2",
"@sendgrid/mail": "8.1.5",
"@vitejs/plugin-react": "4.4.1",
"@vitejs/plugin-react": "4.7.0",
"better-sqlite3": "^11.10.0",
"body-parser": "2.2.0",
"cheerio": "^1.0.0",
"cookie-session": "2.1.0",
"cheerio": "^1.1.0",
"cookie-session": "2.1.1",
"handlebars": "4.7.8",
"highcharts": "12.2.0",
"highcharts": "12.3.0",
"highcharts-react-official": "3.2.2",
"lodash": "4.17.21",
"lowdb": "6.0.1",
@@ -70,10 +70,10 @@
"node-fetch": "3.3.2",
"node-mailjet": "6.0.8",
"package-up": "^5.0.0",
"puppeteer": "^24.8.2",
"puppeteer": "^24.14.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"query-string": "9.1.2",
"query-string": "9.2.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "9.2.0",
@@ -81,28 +81,28 @@
"react-router-dom": "5.3.0",
"redux": "5.0.1",
"redux-thunk": "3.1.0",
"restana": "4.9.9",
"serve-static": "1.16.2",
"restana": "5.0.0",
"serve-static": "2.2.0",
"slack": "11.0.2",
"string-similarity": "^4.0.4",
"vite": "5.4.11"
"vite": "7.0.5"
},
"devDependencies": {
"@babel/core": "7.27.1",
"@babel/eslint-parser": "7.27.1",
"@babel/core": "7.27.3",
"@babel/eslint-parser": "7.27.5",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"chai": "5.2.0",
"chai": "5.2.1",
"eslint": "8.56.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-react": "7.37.4",
"esmock": "2.7.0",
"eslint-plugin-react": "7.37.5",
"esmock": "2.7.1",
"history": "5.3.0",
"husky": "9.1.7",
"less": "4.3.0",
"less": "4.4.0",
"lint-staged": "15.5.2",
"mocha": "10.8.2",
"prettier": "3.5.3",
"prettier": "3.6.2",
"redux-logger": "3.0.6"
}
}

View File

@@ -34,6 +34,7 @@ describe('similarityCheck', () => {
check.setCacheEntry(
'where |X| and |Y| are the cardinalities of the two sets (i.e. the number of elements in each set). The Sørensen index equals twice the number of elements common to both sets divided by the sum of the number of elements in each set.',
);
expect(check.hasSimilarEntries('unrelated text')).to.be.false;
});
});
});

View File

@@ -30,7 +30,7 @@ export default function ProviderTable({ providerData = [], onRemove } = {}) {
render: (_, record) => {
return (
<div style={{ float: 'right' }}>
<Button type="danger" icon={<IconDelete />} onClick={() => onRemove(record.id)} />
<Button type="danger" icon={<IconDelete />} onClick={() => onRemove(record.url)} />
</div>
);
},

View File

@@ -4,6 +4,7 @@ import Logo from '../logo/Logo.jsx';
import {xhrPost} from '../../services/xhr.js';
import './TrackingModal.less';
import inDevelopment from '../../services/developmentMode.js';
const saveResponse = async (analyticsEnabled) => {
await xhrPost('/api/admin/generalSettings', {
@@ -12,6 +13,10 @@ const saveResponse = async (analyticsEnabled) => {
};
export default function TrackingModal() {
if(inDevelopment()){
console.log("FFFUUUCCCKKK")
return null;
}
return <Modal
visible={true}

View File

@@ -0,0 +1,4 @@
export default function isDevelopmentMode(){
const inDevMode= import.meta.env.MODE;
return inDevMode != null && inDevMode === 'development';
}

View File

@@ -94,7 +94,7 @@ export default function JobMutator() {
<form>
<SegmentPart name="Name">
<Input
autofocus
autoFocus
type="text"
maxLength={40}
placeholder="Name"
@@ -124,8 +124,8 @@ export default function JobMutator() {
<ProviderTable
providerData={providerData}
onRemove={(providerId) => {
setProviderData(providerData.filter((provider) => provider.id !== providerId));
onRemove={(providerUrl) => {
setProviderData(providerData.filter((provider) => provider.url !== providerUrl));
}}
/>
</SegmentPart>

1310
yarn.lock

File diff suppressed because it is too large Load Diff