mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
WIP
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null}
|
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":false}
|
||||||
@@ -2,10 +2,13 @@ import { markdown2Html } from '../../services/markdown.js';
|
|||||||
import { getJob } from '../../services/storage/jobStorage.js';
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
const MAX_ENTITIES_PER_CHUNK = 8;
|
const MAX_ENTITIES_PER_CHUNK = 8;
|
||||||
const RATE_LIMIT_INTERVAL = 1000;
|
const RATE_LIMIT_INTERVAL = 1000;
|
||||||
const chatThrottleMap = new Map();
|
const chatThrottleMap = new Map();
|
||||||
|
const pollingTokens = new Set();
|
||||||
|
const updateOffsets = new Map();
|
||||||
|
|
||||||
function cleanupOldThrottles() {
|
function cleanupOldThrottles() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -44,55 +47,120 @@ function getThrottled(chatId, call) {
|
|||||||
return newThrottle.throttled;
|
return newThrottle.throttled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getCallbackUpdates(token) {
|
||||||
* splitting an array into chunks because Telegram only allows for messages up to
|
const offset = updateOffsets.get(token) || 0;
|
||||||
* 4096 chars, thus we have to split messages into chunks
|
return fetch(
|
||||||
* @param inputArray
|
`https://api.telegram.org/bot${token}/getUpdates?allowed_updates=["callback_query"]&timeout=30&offset=${offset}`,
|
||||||
* @param perChunk
|
{
|
||||||
*/
|
method: 'get',
|
||||||
const arrayChunks = (inputArray, perChunk) =>
|
headers: { 'Content-Type': 'application/json' },
|
||||||
inputArray.reduce((all, one, i) => {
|
},
|
||||||
const ch = Math.floor(i / perChunk);
|
);
|
||||||
all[ch] = [].concat(all[ch] || [], one);
|
|
||||||
return all;
|
|
||||||
}, []);
|
|
||||||
function shorten(str, len = 30) {
|
|
||||||
return str.length > len ? str.substring(0, len) + '...' : str;
|
|
||||||
}
|
}
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
|
||||||
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
|
||||||
const job = getJob(jobKey);
|
|
||||||
const jobName = job == null ? jobKey : job.name;
|
|
||||||
const chunks = arrayChunks(newListings, MAX_ENTITIES_PER_CHUNK);
|
|
||||||
|
|
||||||
const getThrottledSend = getThrottled(chatId, async function (body) {
|
function getThrottledSend(token, chatId) {
|
||||||
|
return getThrottled(chatId, async function (body) {
|
||||||
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const promises = chunks.map((chunk) => {
|
async function pollCallbackUpdates(token) {
|
||||||
|
setTimeout(() => pollCallbackUpdates(token), 1000);
|
||||||
|
|
||||||
|
let callbackUpdates;
|
||||||
|
try {
|
||||||
|
callbackUpdates = await getCallbackUpdates(token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('An error occurred when polling callback updates.', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatesData = await callbackUpdates.json();
|
||||||
|
|
||||||
|
if (!updatesData.ok || !updatesData.result || updatesData.result.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each callback query
|
||||||
|
for (const update of updatesData.result) {
|
||||||
|
if (update.callback_query) {
|
||||||
|
const callbackQuery = update.callback_query;
|
||||||
|
const callbackQueryId = callbackQuery.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Answer the callback query to remove the loading state
|
||||||
|
await fetch(`https://api.telegram.org/bot${token}/answerCallbackQuery`, {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify({
|
||||||
|
callback_query_id: callbackQueryId,
|
||||||
|
text: '✅ Opening listing...',
|
||||||
|
show_alert: false,
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error answering callback query:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update offset to avoid processing the same update again
|
||||||
|
updateOffsets.set(token, update.update_id + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCallbackPolling(token) {
|
||||||
|
if (!pollingTokens.has(token)) {
|
||||||
|
pollingTokens.add(token);
|
||||||
|
pollCallbackUpdates(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
|
const throttledSend = getThrottledSend(token, chatId);
|
||||||
|
|
||||||
|
// Start callback polling for this token if not already started
|
||||||
|
startCallbackPolling(token);
|
||||||
|
|
||||||
|
const job = getJob(jobKey);
|
||||||
|
const jobName = job == null ? jobKey : job.name;
|
||||||
|
const chunks = lodash.chunk(newListings, MAX_ENTITIES_PER_CHUNK);
|
||||||
|
|
||||||
|
const promises = chunks.map((listingsInChunk, chunkIndex) => {
|
||||||
const messageParagraphs = [];
|
const messageParagraphs = [];
|
||||||
|
const inlineButtons = [];
|
||||||
|
|
||||||
messageParagraphs.push(`<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:`);
|
|
||||||
messageParagraphs.push(
|
messageParagraphs.push(
|
||||||
...chunk.map(
|
`<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings (${chunkIndex + 1}/${chunks.length}):`,
|
||||||
(o) =>
|
|
||||||
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
|
||||||
[o.address, o.price, o.size].join(' | '),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const body = {
|
listingsInChunk.forEach((listing, listingIndex) => {
|
||||||
|
const normalizedTitle = listing.title.replace(/\*/g, '').trim();
|
||||||
|
const titleExcerpt = lodash.truncate(normalizedTitle, { length: 45, omission: '…' });
|
||||||
|
|
||||||
|
messageParagraphs.push(`
|
||||||
|
${listingIndex + 1}. <a href='${listing.link}'><b>${titleExcerpt}</b></a>
|
||||||
|
${[listing.address, listing.price, listing.size].join(' | ')}`);
|
||||||
|
|
||||||
|
inlineButtons.push({
|
||||||
|
text: `${listingIndex + 1}`,
|
||||||
|
url: listing.link,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return throttledSend({
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
text: messageParagraphs.join('\n\n'),
|
text: messageParagraphs.join('\n\n'),
|
||||||
parse_mode: 'HTML',
|
parse_mode: 'HTML',
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
};
|
reply_markup: {
|
||||||
|
inline_keyboard: lodash.chunk(inlineButtons, 4),
|
||||||
return getThrottledSend(body);
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,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/index.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
async function getListings(url) {
|
async function getListings(url) {
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ export function convertWebToMobile(webUrl) {
|
|||||||
const mobileParams = {
|
const mobileParams = {
|
||||||
searchType: isRadius ? 'radius' : 'region',
|
searchType: isRadius ? 'radius' : 'region',
|
||||||
realestatetype: realType,
|
realestatetype: realType,
|
||||||
|
sorting: '-firstactivation',
|
||||||
|
pagesize: 25,
|
||||||
...(isRadius ? {} : { geocodes }),
|
...(isRadius ? {} : { geocodes }),
|
||||||
...additionalParamsFromWebPath,
|
...additionalParamsFromWebPath,
|
||||||
};
|
};
|
||||||
@@ -180,9 +182,10 @@ export function convertWebToMobile(webUrl) {
|
|||||||
...(currentEquipmentParams ?? []),
|
...(currentEquipmentParams ?? []),
|
||||||
...items.map((item) => EQUIPMENT_MAP[item.toLowerCase()]).filter(Boolean),
|
...items.map((item) => EQUIPMENT_MAP[item.toLowerCase()]).filter(Boolean),
|
||||||
];
|
];
|
||||||
} else {
|
return;
|
||||||
mobileParams[PARAM_NAME_MAP[key]] = val;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mobileParams[PARAM_NAME_MAP[key]] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileQuery = queryString.stringify(mobileParams, {
|
const mobileQuery = queryString.stringify(mobileParams, {
|
||||||
1
lib/services/immoscout/index.js
Normal file
1
lib/services/immoscout/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './convertWebToMobile.js';
|
||||||
Reference in New Issue
Block a user