259 lines
6.8 KiB
JavaScript
259 lines
6.8 KiB
JavaScript
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,
|
|
},
|
|
}],
|
|
}; |