mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
131
lib/notification/adapter/discord_webhook.js
Normal file
131
lib/notification/adapter/discord_webhook.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import { normalizeImageUrl } from '../../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an idempotent decimal color code. The input string-based color code is
|
||||||
|
* generated using the djb2 hash algorithm.
|
||||||
|
*
|
||||||
|
* @param {string} str - Input string as color code base
|
||||||
|
* @returns {number} Generated decimal color code (0 - 16777215)
|
||||||
|
*/
|
||||||
|
const generateColorFromString = (str) => {
|
||||||
|
let hash = 5381; // initial value
|
||||||
|
const input = String(str);
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
// hash * 33 + charCode
|
||||||
|
hash = ((hash << 5) + hash) + input.charCodeAt(i);
|
||||||
|
// Ensure the hash is 32 bit
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let positiveHash = hash >>> 0;
|
||||||
|
const maxColorValue = 16777215;
|
||||||
|
const colorDecimal = positiveHash % maxColorValue;
|
||||||
|
|
||||||
|
return colorDecimal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an embed per listing
|
||||||
|
* (-> see https://birdie0.github.io/discord-webhooks-guide/structure/embeds.html).
|
||||||
|
*
|
||||||
|
* @param {string} jobKey - Key of job (used to set embed color)
|
||||||
|
* @param {object} listing - Object holding listing details
|
||||||
|
* @returns {object} Discord webhook embed
|
||||||
|
*/
|
||||||
|
const buildEmbed = (jobKey, listing) => {
|
||||||
|
const maxTitleLength = 252; // Max embed title length is 256 characters
|
||||||
|
let title = String(listing.title ?? 'N/A');
|
||||||
|
if (title.length > maxTitleLength) {
|
||||||
|
title = title.substring(0, maxTitleLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
name: 'Price',
|
||||||
|
value: String(listing.price ?? 'n/a'),
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Size',
|
||||||
|
value: listing?.size?.replace(/2m/g, 'm²') ?? 'n/a',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Address',
|
||||||
|
value: String(listing.address ?? 'n/a'),
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
title: title,
|
||||||
|
color: generateColorFromString(jobKey),
|
||||||
|
url: listing.link,
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listing.image) {
|
||||||
|
embed.image = {
|
||||||
|
url: normalizeImageUrl(listing.image),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed
|
||||||
|
};
|
||||||
|
|
||||||
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const adapter = notificationConfig.find((adapter) => adapter.id === config.id);
|
||||||
|
const webhookUrl = adapter?.fields?.webhookUrl;
|
||||||
|
if (!webhookUrl || newListings.length == 0) return Promise.resolve([]);
|
||||||
|
|
||||||
|
const job = getJob(jobKey);
|
||||||
|
const jobName = job?.name || jobKey;
|
||||||
|
|
||||||
|
const embeds = newListings.map((listing) => buildEmbed(jobKey, listing));
|
||||||
|
|
||||||
|
const maxEmbedsPerMessage = 10; // Discord only allows up to 10 embeds
|
||||||
|
const webhookPromises = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < embeds.length; i += maxEmbedsPerMessage) {
|
||||||
|
// Send multiple Discord messages with up to 10 embeds per message
|
||||||
|
const embedChunk = embeds.slice(i, i + maxEmbedsPerMessage);
|
||||||
|
|
||||||
|
const content = i === 0 ? `*${jobName}:* ${serviceName} found **${newListings.length}** new listings.` : '';
|
||||||
|
const body = JSON.stringify({
|
||||||
|
content: content,
|
||||||
|
embeds: embedChunk,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchPromise = fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`Error sending Discord webhook for chunk starting at ${i}:`, error);
|
||||||
|
return Promise.reject(new Error(`Webhook failed: ${error.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
webhookPromises.push(fetchPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.allSettled(webhookPromises);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
id: 'discord_webhook',
|
||||||
|
name: 'Discord Webhook',
|
||||||
|
readme: markdown2Html('lib/notification/adapter/discord_webhook.md'),
|
||||||
|
description: 'Fredy will send new listings to the Discord channel of your choice.',
|
||||||
|
fields: {
|
||||||
|
webhookUrl: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Webhook URL',
|
||||||
|
description: 'The URL of the Discord webhook to send messages to.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
4
lib/notification/adapter/discord_webhook.md
Normal file
4
lib/notification/adapter/discord_webhook.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
### Discord Adapter
|
||||||
|
|
||||||
|
To use the [Discord](https://discord.com/) Adapter, you need to create a webhook on the Discord channel of your choice. You can follow the instructions of _Making A Webhook_ on [this support website](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).
|
||||||
|
Once you have created a webhook, copy and paste the webhook URL.
|
||||||
Reference in New Issue
Block a user