289 lines
8.0 KiB
JavaScript
289 lines
8.0 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const axios = require('axios');
|
|
const cron = require('node-cron');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
const PRICE_THRESHOLD = 650; // USD
|
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
const EBAY_CLIENT_ID = process.env.EBAY_CLIENT_ID;
|
|
const EBAY_CLIENT_SECRET = process.env.EBAY_CLIENT_SECRET;
|
|
|
|
// File path for notified items
|
|
const NOTIFIED_ITEMS_FILE = path.join(__dirname, 'notified.json');
|
|
|
|
// Load notified items from file
|
|
function loadNotifiedItems() {
|
|
try {
|
|
if (fs.existsSync(NOTIFIED_ITEMS_FILE)) {
|
|
const data = fs.readFileSync(NOTIFIED_ITEMS_FILE, 'utf8');
|
|
return new Map(JSON.parse(data));
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠ Failed to load notified items file:', error.message);
|
|
}
|
|
return new Map();
|
|
}
|
|
|
|
// Save notified items to file
|
|
function saveNotifiedItems() {
|
|
try {
|
|
const data = JSON.stringify([...notifiedItems.entries()]);
|
|
fs.writeFileSync(NOTIFIED_ITEMS_FILE, data);
|
|
} catch (error) {
|
|
console.error('✗ Failed to save notified items:', error.message);
|
|
}
|
|
}
|
|
|
|
// Notification history (store last 50 notified items)
|
|
let notifiedItems = loadNotifiedItems(); // key: legacyItemId, value: timestamp
|
|
|
|
/**
|
|
* Check if item was already notified
|
|
*/
|
|
function isAlreadyNotified(legacyItemId) {
|
|
return notifiedItems.has(legacyItemId);
|
|
}
|
|
|
|
/**
|
|
* Add item to notified list
|
|
*/
|
|
function addToNotified(legacyItemId) {
|
|
notifiedItems.set(legacyItemId, Date.now());
|
|
|
|
// Keep only last 50 items
|
|
if (notifiedItems.size > 50) {
|
|
const oldestKey = notifiedItems.keys().next().value;
|
|
notifiedItems.delete(oldestKey);
|
|
}
|
|
|
|
// Save to file
|
|
saveNotifiedItems();
|
|
}
|
|
|
|
// Exchange rate cache (AUD to USD)
|
|
let audToUsdRate = 0.65;
|
|
let rateExpireAt = 0;
|
|
|
|
/**
|
|
* Get real-time AUD to USD exchange rate
|
|
*/
|
|
async function getExchangeRate() {
|
|
const now = Date.now();
|
|
|
|
// Return cached rate if still valid (1 hour)
|
|
if (audToUsdRate && now < rateExpireAt) {
|
|
return audToUsdRate;
|
|
}
|
|
|
|
try {
|
|
const res = await axios.get(
|
|
'https://api.exchangerate-api.com/v4/latest/AUD',
|
|
{ timeout: 5000 }
|
|
);
|
|
audToUsdRate = res.data.rates?.USD || 0.65;
|
|
rateExpireAt = now + 60 * 60 * 1000; // Cache for 1 hour
|
|
console.log(`💱 Exchange rate updated: 1 AUD = ${audToUsdRate} USD`);
|
|
return audToUsdRate;
|
|
} catch (error) {
|
|
console.warn('⚠ Failed to fetch exchange rate, using cached value');
|
|
return audToUsdRate || 0.65;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert price to USD
|
|
*/
|
|
async function convertToUSD(price, currency) {
|
|
if (currency === 'USD') return price;
|
|
if (currency === 'AUD') {
|
|
const rate = await getExchangeRate();
|
|
return price * rate;
|
|
}
|
|
return price; // Default: assume USD
|
|
}
|
|
|
|
// ========================== EBAY AUTH ==========================
|
|
let ebayTokenCache = { token: null, expireAt: 0 };
|
|
|
|
async function getEbayAccessToken() {
|
|
const now = Date.now();
|
|
if (ebayTokenCache.token && now < ebayTokenCache.expireAt) {
|
|
return ebayTokenCache.token;
|
|
}
|
|
|
|
const auth = Buffer.from(
|
|
`${EBAY_CLIENT_ID}:${EBAY_CLIENT_SECRET}`
|
|
).toString("base64");
|
|
|
|
const res = await axios.post(
|
|
"https://api.ebay.com/identity/v1/oauth2/token",
|
|
"grant_type=client_credentials&scope=https://api.ebay.com/oauth/api_scope",
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Authorization: `Basic ${auth}`
|
|
}
|
|
}
|
|
);
|
|
|
|
ebayTokenCache.token = res.data.access_token;
|
|
ebayTokenCache.expireAt =
|
|
now + (res.data.expires_in - 60) * 1000;
|
|
|
|
return ebayTokenCache.token;
|
|
}
|
|
|
|
// ========================== SEARCH EBAY ITEMS ==========================
|
|
async function searchEbayItems({ q, marketplace = "EBAY_US" }) {
|
|
const token = await getEbayAccessToken();
|
|
|
|
const res = await axios.get(
|
|
"https://api.ebay.com/buy/browse/v1/item_summary/search",
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"X-EBAY-C-MARKETPLACE-ID": marketplace
|
|
},
|
|
params: {
|
|
q,
|
|
limit: 50,
|
|
sort: "newlyListed"
|
|
},
|
|
timeout: 20000
|
|
}
|
|
);
|
|
|
|
return (res.data.itemSummaries || []).map(item => ({
|
|
legacyItemId: item.legacyItemId,
|
|
title: item.title,
|
|
price: item.price?.value,
|
|
currency: item.price?.currency,
|
|
condition: item.condition,
|
|
seller: item.seller?.username,
|
|
image: item.image?.imageUrl
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Send notification to Telegram group
|
|
*/
|
|
async function notifyTelegram(message) {
|
|
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
|
console.warn('⚠ Telegram configuration missing, skipping notification');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await axios.post(
|
|
`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
|
|
{
|
|
chat_id: TELEGRAM_CHAT_ID,
|
|
text: message,
|
|
parse_mode: 'HTML'
|
|
}
|
|
);
|
|
// console.log(message);
|
|
console.log('✓ Telegram notification sent');
|
|
} catch (error) {
|
|
console.error('✗ Failed to send Telegram notification:', error.response?.data || error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check items and notify if price is below threshold
|
|
*/
|
|
async function checkAndNotify(keyword, marketplace = "EBAY_US") {
|
|
console.log(`\n[${new Date().toISOString()}] Checking items for: ${keyword} (${marketplace})`);
|
|
|
|
try {
|
|
const items = await searchEbayItems({ q: keyword, marketplace });
|
|
|
|
if (items.length === 0) {
|
|
console.log('No items found');
|
|
return;
|
|
}
|
|
|
|
// Find items below price threshold (skip already notified)
|
|
const cheapItems = [];
|
|
for (const item of items) {
|
|
if (isAlreadyNotified(item.legacyItemId)) {
|
|
continue;
|
|
}
|
|
const priceInUSD = await convertToUSD(parseFloat(item.price || 0), item.currency);
|
|
if (priceInUSD < PRICE_THRESHOLD) {
|
|
cheapItems.push({ ...item, priceInUSD });
|
|
addToNotified(item.legacyItemId);
|
|
}
|
|
}
|
|
|
|
if (cheapItems.length > 0) {
|
|
console.log(`Found ${cheapItems.length} item(s) below $${PRICE_THRESHOLD}`);
|
|
|
|
const message = `========= ${keyword.toUpperCase()} (${marketplace}) ==========\n` +
|
|
cheapItems.slice(0, 10).map(item =>
|
|
`${item.title}\n$${item.price} ${item.currency || 'USD'}\nhttps://${item.currency === 'USD' ? 'www.ebay.com' : 'www.ebay.com.au'}/itm/${item.legacyItemId}`
|
|
).join('\n==================================\n') +
|
|
'\n==================================';
|
|
|
|
await notifyTelegram(message);
|
|
} else {
|
|
console.log(`No items found below $${PRICE_THRESHOLD} USD`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error checking items:', error.message);
|
|
}
|
|
}
|
|
|
|
// Middleware
|
|
app.use(express.json());
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Manual trigger endpoint
|
|
app.post('/check', express.json(), async (req, res) => {
|
|
const { keyword, marketplace } = req.body;
|
|
|
|
if (!keyword) {
|
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
}
|
|
|
|
await checkAndNotify(keyword, marketplace);
|
|
res.json({ success: true, message: 'Check completed' });
|
|
});
|
|
|
|
// eBay Search Configurations
|
|
const EBAY_SEARCH_CONFIGS = [
|
|
{
|
|
marketplace: "EBAY_US",
|
|
q: "MACBOOK PRO M1 32GB",
|
|
},
|
|
{
|
|
marketplace: "EBAY_AU",
|
|
q: "MACBOOK PRO M1 32GB",
|
|
},
|
|
];
|
|
|
|
// Run every minute
|
|
cron.schedule('* * * * *', async () => {
|
|
console.log('\n=== Running scheduled check ===');
|
|
|
|
for (const config of EBAY_SEARCH_CONFIGS) {
|
|
await checkAndNotify(config.q, config.marketplace);
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 Server running on port ${PORT}`);
|
|
console.log(`⏰ Scheduled check running every minute`);
|
|
console.log(`💰 Price threshold: $${PRICE_THRESHOLD} USD`);
|
|
}); |