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 fetch from 'node-fetch';
|
||||
import pThrottle from 'p-throttle';
|
||||
import lodash from 'lodash';
|
||||
|
||||
const MAX_ENTITIES_PER_CHUNK = 8;
|
||||
const RATE_LIMIT_INTERVAL = 1000;
|
||||
const chatThrottleMap = new Map();
|
||||
const pollingTokens = new Set();
|
||||
const updateOffsets = new Map();
|
||||
|
||||
function cleanupOldThrottles() {
|
||||
const now = Date.now();
|
||||
@@ -44,55 +47,120 @@ function getThrottled(chatId, call) {
|
||||
return newThrottle.throttled;
|
||||
}
|
||||
|
||||
/**
|
||||
* splitting an array into chunks because Telegram only allows for messages up to
|
||||
* 4096 chars, thus we have to split messages into chunks
|
||||
* @param inputArray
|
||||
* @param perChunk
|
||||
*/
|
||||
const arrayChunks = (inputArray, perChunk) =>
|
||||
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;
|
||||
function getCallbackUpdates(token) {
|
||||
const offset = updateOffsets.get(token) || 0;
|
||||
return fetch(
|
||||
`https://api.telegram.org/bot${token}/getUpdates?allowed_updates=["callback_query"]&timeout=30&offset=${offset}`,
|
||||
{
|
||||
method: 'get',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
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`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify(body),
|
||||
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 inlineButtons = [];
|
||||
|
||||
messageParagraphs.push(`<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:`);
|
||||
messageParagraphs.push(
|
||||
...chunk.map(
|
||||
(o) =>
|
||||
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
||||
[o.address, o.price, o.size].join(' | '),
|
||||
),
|
||||
`<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings (${chunkIndex + 1}/${chunks.length}):`,
|
||||
);
|
||||
|
||||
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,
|
||||
text: messageParagraphs.join('\n\n'),
|
||||
parse_mode: 'HTML',
|
||||
disable_web_page_preview: true,
|
||||
};
|
||||
|
||||
return getThrottledSend(body);
|
||||
reply_markup: {
|
||||
inline_keyboard: lodash.chunk(inlineButtons, 4),
|
||||
},
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
*/
|
||||
|
||||
import utils, { buildHash } from '../utils.js';
|
||||
import { convertWebToMobile } from '../services/immoscout/immoscout-web-translator.js';
|
||||
import { convertWebToMobile } from '../services/immoscout/index.js';
|
||||
let appliedBlackList = [];
|
||||
|
||||
async function getListings(url) {
|
||||
|
||||
@@ -164,6 +164,8 @@ export function convertWebToMobile(webUrl) {
|
||||
const mobileParams = {
|
||||
searchType: isRadius ? 'radius' : 'region',
|
||||
realestatetype: realType,
|
||||
sorting: '-firstactivation',
|
||||
pagesize: 25,
|
||||
...(isRadius ? {} : { geocodes }),
|
||||
...additionalParamsFromWebPath,
|
||||
};
|
||||
@@ -180,9 +182,10 @@ export function convertWebToMobile(webUrl) {
|
||||
...(currentEquipmentParams ?? []),
|
||||
...items.map((item) => EQUIPMENT_MAP[item.toLowerCase()]).filter(Boolean),
|
||||
];
|
||||
} else {
|
||||
mobileParams[PARAM_NAME_MAP[key]] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
mobileParams[PARAM_NAME_MAP[key]] = val;
|
||||
}
|
||||
|
||||
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