const express = require('express'); const cors = require('cors'); const { default: axios } = require('axios'); const path = require('path'); require('dotenv').config({ path: path.resolve(__dirname, '.env') }); const app = express(); const port = process.env.PORT || 3002; const META_API_VERSION = 'v21.0'; const META_API_BASE_URL = `https://graph.facebook.com/${META_API_VERSION}`; const META_ACCESS_TOKEN = process.env.META_ACCESS_TOKEN; const AD_ACCOUNT_ID = process.env.META_AD_ACCOUNT_ID; app.use(cors()); app.use(express.json()); const metaApiRequest = async (endpoint, params = {}) => { try { const response = await axios.get(`${META_API_BASE_URL}/${endpoint}`, { params: { access_token: META_ACCESS_TOKEN, time_zone: 'America/New_York', ...params, }, }); return response.data; } catch (error) { console.error('Meta API Error:', { message: error.message, response: error.response?.data, endpoint, }); throw error; } }; const fetchCampaigns = async (since, until) => { const campaigns = await metaApiRequest(`act_${AD_ACCOUNT_ID}/campaigns`, { fields: ` id, name, status, objective, daily_budget, lifetime_budget, adsets{daily_budget,lifetime_budget}, insights.time_range({"since":"${since}","until":"${until}"}).level(campaign){ spend, impressions, clicks, ctr, cpc, reach, frequency, cpm, actions, action_values, cost_per_action_type } `, limit: 100, }); // Filter campaigns with valid spend const filtered = campaigns.data.filter(c => c.insights?.data?.[0]?.spend > 0); return filtered; }; app.get('/campaigns', async (req, res) => { try { const { since, until } = req.query; if (!since || !until) { return res.status(400).json({ error: 'Date range is required (since, until)' }); } const campaigns = await metaApiRequest(`act_${AD_ACCOUNT_ID}/campaigns`, { fields: [ 'id', 'name', 'status', 'objective', 'daily_budget', 'lifetime_budget', 'adsets{daily_budget,lifetime_budget}', `insights.time_range({'since':'${since}','until':'${until}'}).level(campaign){ spend, impressions, clicks, ctr, reach, frequency, cpm, cpc, actions, action_values, cost_per_action_type }`, ].join(','), limit: 100, time_zone: 'America/New_York', // Specify Eastern Time }); const filteredCampaigns = campaigns.data.filter(c => c.insights?.data?.[0]?.spend > 0); res.json(filteredCampaigns); } catch (error) { console.error('Campaign fetch error:', error); res.status(500).json({ error: 'Failed to fetch campaigns', details: error.response?.data?.error?.message || error.message, }); } }); app.get('/insights', async (req, res) => { try { const { timeframe = '30' } = req.query; const since = new Date(); since.setDate(since.getDate() - parseInt(timeframe)); const insights = await metaApiRequest(`act_${AD_ACCOUNT_ID}/insights`, { fields: 'spend,impressions,clicks,ctr,reach,frequency,cpm,actions,action_values', time_increment: 1, time_range: JSON.stringify({ since: since.toISOString().split('T')[0], until: new Date().toISOString().split('T')[0] }) }); res.json(insights.data); } catch (error) { res.status(500).json({ error: 'Failed to fetch insights' }); } }); app.get('/campaigns', async (req, res) => { try { const { since, until } = req.query; if (!since || !until) { return res.status(400).json({ error: 'Date range is required (since, until)' }); } const campaigns = await fetchCampaigns(since, until); campaigns.forEach((campaign) => { const insights = campaign.insights?.data?.[0]; }); res.json(campaigns); } catch (error) { console.error('Campaign fetch error:', error.message); res.status(500).json({ error: 'Failed to fetch campaigns', details: error.response?.data?.error?.message || error.message, }); } }); app.get('/account_insights', async (req, res) => { try { const { since, until } = req.query; if (!since || !until) { return res.status(400).json({ error: 'Date range is required (since, until)' }); } const accountInsights = await metaApiRequest(`act_${AD_ACCOUNT_ID}/insights`, { fields: 'reach', time_range: JSON.stringify({ since, until }), time_zone: 'America/New_York', // Specify Eastern Time }); // accountInsights.data[0] contains the aggregated metrics over the date range res.json(accountInsights.data[0]); } catch (error) { console.error('Account insights fetch error:', error); res.status(500).json({ error: 'Failed to fetch account insights', details: error.response?.data?.error?.message || error.message, }); } }); app.patch('/campaigns/:campaignId/budget', async (req, res) => { try { const { campaignId } = req.params; const { budget } = req.body; const response = await axios.post(`${META_API_BASE_URL}/${campaignId}`, { access_token: META_ACCESS_TOKEN, daily_budget: budget * 100, // Convert to cents }); res.json(response.data); } catch (error) { res.status(500).json({ error: 'Failed to update campaign budget' }); } }); app.post('/campaigns/:campaignId/:action', async (req, res) => { try { const { campaignId, action } = req.params; const status = action === 'pause' ? 'PAUSED' : 'ACTIVE'; const response = await axios.post(`${META_API_BASE_URL}/${campaignId}`, { access_token: META_ACCESS_TOKEN, status, }); res.json(response.data); } catch (error) { res.status(500).json({ error: `Failed to ${req.params.action} campaign` }); } }); // Rate limit handling const handleRateLimit = (headers) => { const usageInfo = headers['x-business-use-case-usage']; if (usageInfo) { try { const usage = JSON.parse(usageInfo); const adsApiAccess = Object.values(usage)[0]?.[0]; if (adsApiAccess?.estimated_time_to_regain_access > 0) { throw new Error(`Rate limit exceeded. Try again in ${adsApiAccess.estimated_time_to_regain_access} seconds`); } } catch (e) { console.error('Error parsing rate limit headers:', e); } } }; // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' }); }); // Start server app.listen(port, () => { }); // PM2 process name module.exports = { apps: [{ name: 'meta-ads-server', script: 'server.js', env: { NODE_ENV: 'production', PORT: 3004, }, }], };