Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the Telegram adapter to add a rich message option with a photo #106

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ db/
npm-debug.log
.DS_Store
.idea
.vscode/
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function applyBlacklist(o) {

const config = {
url: null,
//this is the container wrapping the search listings
//this is the container wrapping the search listings
crawlContainer: '#result-list-stage .item',
crawlFields: {
id: '@id',
Expand All @@ -36,6 +36,9 @@ const config = {
title: '.item a img@title',
link: 'a[id*="lnkImgToDetails_"]@href',
address: '.item .box-25 .ellipsis .text-100 | removeNewline | trim',
image: 'img@src',
//some websites provide image URLs for rendered ads only and lazy URLs for the rest
lazyImage: 'lazyImg@src'
},
paginate: '#idResultList .margin-bottom-6.margin-bottom-sm-12 .panel a.pull-right@href',
normalize: normalize,
Expand Down
16 changes: 16 additions & 0 deletions lib/api/routes/notificationAdapterRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ notificationAdapterRouter.post('/try', async (req, res) => {
size: '666 2m',
link: 'https://www.orange-coding.net',
},
{
price: '1500 €',
title:
'This is a test listing with long and doubled title. This is a test listing with long and doubled title.',
address: 'some address',
size: '777 2m',
link: 'https://www.orange-coding.net',
},
{
price: '2500 €',
title: 'This is a test listing with an image',
address: 'some address',
size: '555 2m',
link: 'https://www.orange-coding.net',
image: 'https://images.pexels.com/photos/106399/pexels-photo-106399.jpeg',
},
],
notificationConfig,
jobKey: 'TestJob',
Expand Down
2 changes: 1 addition & 1 deletion lib/notification/adapter/apprise.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
const jobName = job == null ? jobKey : job.name;
const promises = newListings.map((newListing) => {
const title = `${jobName} at ${serviceName}: ${newListing.title}`;
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\Link: ${newListing.link}`;
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\nLink: ${newListing.link}`;
return fetch(server, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand Down
134 changes: 103 additions & 31 deletions lib/notification/adapter/telegram.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,115 @@ const arrayChunks = (inputArray, perChunk) =>
all[ch] = [].concat(all[ch] || [], one);
return all;
}, []);
function shorten(str, len = 30) {

function shorten(str, len = 45) {
return str.length > len ? str.substring(0, len) + '...' : str;
}
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {

function isValidURL(url) {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
}

const sendTelegramMessage = (token, chatId, message, isPhoto = false, photoUrl = '') => {
let url = ''
let body = ''

if (isPhoto) {
url = `https://api.telegram.org/bot${token}/sendPhoto`;
body = JSON.stringify({
chat_id: chatId,
photo: photoUrl,
caption: message,
parse_mode: 'HTML',
});
} else {
url = `https://api.telegram.org/bot${token}/sendMessage`;
body = JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML',
disable_web_page_preview: !isPhoto,
});
}

/**
* This is to not break the rate limit. It is to only send 1 message per second
*/
return new Promise((resolve, reject) => {
setTimeout(() => {
fetch(url, {
method: 'post',
body: body,
headers: { 'Content-Type': 'application/json' },
})
.then(() => {
resolve();
})
.catch(() => {
reject();
});
}, RATE_LIMIT_INTERVAL);
});
};

const sendRichMessage = ({ 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 promises = newListings.map((o, index) => {
let message = '';
/**
* Only send the job information once
*/
if (index === 0) {
message += `<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:\n\n`;
}
message +=
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, '')).trim()}</b></a>\n` +
`🏠 Address: ${o.address}\n` +
`💰 Price: ${o.price}\n` +
`📐 Size: ${o.size}\n`;
const imageURL = o.lazyImage || o.image;
return sendTelegramMessage(token, chatId, message, imageURL && isValidURL(imageURL), imageURL);
});
return Promise.all(promises);
};

const sendPlainMessage = ({ 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;
//we have to split messages into chunk, because otherwise messages are going to become too big and will fail
const chunks = arrayChunks(newListings, MAX_ENTITIES_PER_CHUNK);
const promises = chunks.map((chunk) => {
let message = `<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:\n\n`;
message += 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(' | ') +
'\n\n',
);
/**
* This is to not break the rate limit. It is to only send 1 message per second
*/
return new Promise((resolve, reject) => {
setTimeout(() => {
fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'post',
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
headers: { 'Content-Type': 'application/json' },
})
.then(() => {
resolve();
})
.catch(() => {
reject();
});
}, RATE_LIMIT_INTERVAL);
});
message += chunk
.map(
(o) =>
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, '')).trim()}</b></a>\n` +
[o.address, o.price, o.size].join(' | '),
)
.join('\n\n');
return sendTelegramMessage(token, chatId, message);
});
return Promise.all(promises);
};

export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
const { richMessage } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
if (richMessage) {
return sendRichMessage({ serviceName, newListings, notificationConfig, jobKey });
} else {
return sendPlainMessage({ serviceName, newListings, notificationConfig, jobKey });
}
};

export const config = {
id: 'telegram',
name: 'Telegram',
Expand All @@ -74,5 +140,11 @@ export const config = {
label: 'Chat Id',
description: 'The chat id to send messages to you.',
},
richMessage: {
type: 'boolean',
label: 'Send rich message',
description: 'When selected sends a rich message with image.',
value: false,
},
},
};
1 change: 1 addition & 0 deletions lib/provider/einsAImmobilien.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const config = {
rooms: '.tabelle .inner_object_data .data_boxes div:nth-child(2)',
title: '.tabelle .inner_object_data .tabelle_inhalt_titel_black | removeNewline | trim',
description: '.tabelle .inner_object_data .objekt_beschreibung | removeNewline | trim',
image: '.tabelle .inner_object_pic img@src',
},
normalize: normalize,
filter: applyBlacklist,
Expand Down
2 changes: 2 additions & 0 deletions lib/provider/immoscout.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const config = {
title: '.result-list-entry .result-list-entry__brand-title-container h2 | removeNewline | trim',
link: '.result-list-entry .result-list-entry__brand-title-container@href',
address: '.result-list-entry .result-list-entry__map-link',
image: '.result-list-entry .gallery-container .slick-list .gallery__image@src',
lazyImage: '.result-list-entry .gallery-container .gallery__image@data-lazy-src',
},
paginate: '#pager .align-right a@href',
normalize: normalize,
Expand Down
1 change: 1 addition & 0 deletions lib/provider/kleinanzeigen.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const config = {
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
description: '.aditem-main p:not(.text-module-end) | removeNewline | trim',
address: '.aditem-main--top--left | trim | removeNewline',
image: '.aditem-image .imagebox img@src',
},
paginate: '#srchrslt-pagination .pagination-next@href',
normalize: normalize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ export default function NotificationAdapterMutator({
return (
<Form key={key}>
{uiElement.type === 'boolean' ? (
<Switch
checked={uiElement.value || false}
<Form.Switch
checked={uiElement.checked || false}
label={uiElement.label}
onChange={(checked) => {
setValue(selectedAdapter, uiElement, key, checked);
}}
Expand Down
Loading