meta-ads-mcp 0.2.4__py3-none-any.whl → 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meta_ads_mcp/__init__.py +1 -1
- meta_ads_mcp/core/adsets.py +32 -2
- meta_ads_mcp/core/api.py +68 -24
- meta_ads_mcp/core/auth.py +1212 -382
- meta_ads_mcp/core/authentication.py +10 -1
- meta_ads_mcp/core/utils.py +10 -0
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/METADATA +21 -11
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/RECORD +10 -10
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.4.dist-info → meta_ads_mcp-0.2.5.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/core/auth.py
CHANGED
|
@@ -13,6 +13,7 @@ from urllib.parse import urlparse, parse_qs
|
|
|
13
13
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
14
14
|
import json
|
|
15
15
|
from .utils import logger
|
|
16
|
+
import requests
|
|
16
17
|
|
|
17
18
|
# Auth constants
|
|
18
19
|
AUTH_SCOPE = "ads_management,ads_read,business_management"
|
|
@@ -264,413 +265,1156 @@ class AuthManager:
|
|
|
264
265
|
# Callback Handler class definition
|
|
265
266
|
class CallbackHandler(BaseHTTPRequestHandler):
|
|
266
267
|
def do_GET(self):
|
|
267
|
-
global token_container, auth_manager, needs_authentication
|
|
268
|
+
global token_container, auth_manager, needs_authentication, update_confirmation
|
|
268
269
|
|
|
269
|
-
|
|
270
|
-
#
|
|
271
|
-
self.
|
|
272
|
-
self.send_header("Content-type", "text/html")
|
|
273
|
-
self.end_headers()
|
|
270
|
+
try:
|
|
271
|
+
# Print path for debugging
|
|
272
|
+
print(f"Callback server received request: {self.path}")
|
|
274
273
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
274
|
+
if self.path.startswith("/callback"):
|
|
275
|
+
self.send_response(200)
|
|
276
|
+
self.send_header("Content-type", "text/html")
|
|
277
|
+
self.end_headers()
|
|
278
|
+
|
|
279
|
+
# Get the token from the fragment
|
|
280
|
+
# We need to handle it via JS since the fragment is not sent to the server
|
|
281
|
+
callback_html = """
|
|
282
|
+
<!DOCTYPE html>
|
|
283
|
+
<html>
|
|
284
|
+
<head>
|
|
285
|
+
<title>Authentication Successful</title>
|
|
286
|
+
<style>
|
|
287
|
+
body {
|
|
288
|
+
font-family: Arial, sans-serif;
|
|
289
|
+
line-height: 1.6;
|
|
290
|
+
color: #333;
|
|
291
|
+
max-width: 800px;
|
|
292
|
+
margin: 0 auto;
|
|
293
|
+
padding: 20px;
|
|
294
|
+
}
|
|
295
|
+
.success {
|
|
296
|
+
color: #4CAF50;
|
|
297
|
+
font-size: 24px;
|
|
298
|
+
margin-bottom: 20px;
|
|
299
|
+
}
|
|
300
|
+
.info {
|
|
301
|
+
background-color: #f5f5f5;
|
|
302
|
+
padding: 15px;
|
|
303
|
+
border-radius: 4px;
|
|
304
|
+
margin-bottom: 20px;
|
|
305
|
+
}
|
|
306
|
+
.button {
|
|
307
|
+
background-color: #4CAF50;
|
|
308
|
+
color: white;
|
|
309
|
+
padding: 10px 15px;
|
|
310
|
+
border: none;
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
315
|
+
</head>
|
|
316
|
+
<body>
|
|
317
|
+
<div class="success">Authentication Successful!</div>
|
|
318
|
+
<div class="info">
|
|
319
|
+
<p>Your Meta Ads API token has been received.</p>
|
|
320
|
+
<p>You can now close this window and return to the application.</p>
|
|
321
|
+
</div>
|
|
322
|
+
<button class="button" onclick="window.close()">Close Window</button>
|
|
287
323
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
</
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
324
|
+
<script>
|
|
325
|
+
// Function to parse URL parameters including fragments
|
|
326
|
+
function parseURL(url) {
|
|
327
|
+
var params = {};
|
|
328
|
+
var parser = document.createElement('a');
|
|
329
|
+
parser.href = url;
|
|
330
|
+
|
|
331
|
+
// Parse fragment parameters
|
|
332
|
+
var fragment = parser.hash.substring(1);
|
|
333
|
+
var fragmentParams = fragment.split('&');
|
|
334
|
+
|
|
335
|
+
for (var i = 0; i < fragmentParams.length; i++) {
|
|
336
|
+
var pair = fragmentParams[i].split('=');
|
|
337
|
+
params[pair[0]] = decodeURIComponent(pair[1]);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return params;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Parse the URL to get the access token
|
|
344
|
+
var params = parseURL(window.location.href);
|
|
345
|
+
var token = params['access_token'];
|
|
346
|
+
var expires_in = params['expires_in'];
|
|
347
|
+
|
|
348
|
+
// Send the token to the server
|
|
349
|
+
if (token) {
|
|
350
|
+
// Create XMLHttpRequest object
|
|
351
|
+
var xhr = new XMLHttpRequest();
|
|
352
|
+
|
|
353
|
+
// Configure it to make a GET request to the /token endpoint
|
|
354
|
+
xhr.open('GET', '/token?token=' + encodeURIComponent(token) +
|
|
355
|
+
'&expires_in=' + encodeURIComponent(expires_in), true);
|
|
356
|
+
|
|
357
|
+
// Set up a handler for when the request is complete
|
|
358
|
+
xhr.onload = function() {
|
|
359
|
+
if (xhr.status === 200) {
|
|
360
|
+
console.log('Token successfully sent to server');
|
|
361
|
+
} else {
|
|
362
|
+
console.error('Failed to send token to server');
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Send the request
|
|
367
|
+
xhr.send();
|
|
368
|
+
} else {
|
|
369
|
+
console.error('No token found in URL');
|
|
370
|
+
document.body.innerHTML += '<div style="color: red; margin-top: 20px;">Error: No authentication token found. Please try again.</div>';
|
|
371
|
+
}
|
|
372
|
+
</script>
|
|
373
|
+
</body>
|
|
374
|
+
</html>
|
|
375
|
+
"""
|
|
376
|
+
self.wfile.write(callback_html.encode())
|
|
377
|
+
return
|
|
342
378
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
379
|
+
elif self.path.startswith("/token"):
|
|
380
|
+
# Extract token from query params
|
|
381
|
+
query = parse_qs(urlparse(self.path).query)
|
|
382
|
+
token_container["token"] = query.get("token", [""])[0]
|
|
347
383
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<td>New Value</td>
|
|
354
|
-
<td>Description</td>
|
|
355
|
-
</tr>
|
|
356
|
-
"""
|
|
357
|
-
|
|
358
|
-
# Special handling for frequency_control_specs
|
|
359
|
-
for k, v in changes_dict.items():
|
|
360
|
-
description = ""
|
|
361
|
-
if k == "frequency_control_specs" and isinstance(v, list) and len(v) > 0:
|
|
362
|
-
spec = v[0]
|
|
363
|
-
if all(key in spec for key in ["event", "interval_days", "max_frequency"]):
|
|
364
|
-
description = f"Cap to {spec['max_frequency']} {spec['event'].lower()} per {spec['interval_days']} days"
|
|
384
|
+
if "expires_in" in query:
|
|
385
|
+
try:
|
|
386
|
+
token_container["expires_in"] = int(query.get("expires_in", ["0"])[0])
|
|
387
|
+
except ValueError:
|
|
388
|
+
token_container["expires_in"] = None
|
|
365
389
|
|
|
366
|
-
#
|
|
367
|
-
|
|
390
|
+
# Send success response
|
|
391
|
+
self.send_response(200)
|
|
392
|
+
self.send_header("Content-type", "text/plain")
|
|
393
|
+
self.end_headers()
|
|
394
|
+
self.wfile.write(b"Token received")
|
|
368
395
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
396
|
+
# Process the token (save it) immediately
|
|
397
|
+
if token_container["token"]:
|
|
398
|
+
# Get the short-lived token
|
|
399
|
+
short_lived_token = token_container["token"]
|
|
400
|
+
|
|
401
|
+
# Try to exchange for a long-lived token
|
|
402
|
+
long_lived_token_info = exchange_token_for_long_lived(short_lived_token)
|
|
403
|
+
|
|
404
|
+
if long_lived_token_info:
|
|
405
|
+
# Successfully exchanged for long-lived token
|
|
406
|
+
logger.info(f"Token received and exchanged for long-lived token (expires in {long_lived_token_info.expires_in} seconds)")
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
# Set the token info in the auth_manager
|
|
410
|
+
auth_manager.token_info = long_lived_token_info
|
|
411
|
+
logger.info(f"Long-lived token info set in auth_manager, expires in {long_lived_token_info.expires_in} seconds")
|
|
412
|
+
|
|
413
|
+
# Save to cache
|
|
414
|
+
auth_manager._save_token_to_cache()
|
|
415
|
+
logger.info(f"Long-lived token successfully saved to cache at {auth_manager._get_token_cache_path()}")
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logger.error(f"Error saving long-lived token to cache: {e}")
|
|
418
|
+
else:
|
|
419
|
+
# Fall back to the short-lived token
|
|
420
|
+
logger.warning("Failed to exchange for long-lived token, using short-lived token instead")
|
|
421
|
+
token_info = TokenInfo(
|
|
422
|
+
access_token=token_container["token"],
|
|
423
|
+
expires_in=token_container["expires_in"]
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
# Set the token info in the auth_manager
|
|
428
|
+
auth_manager.token_info = token_info
|
|
429
|
+
logger.info(f"Token info set in auth_manager, expires in {token_info.expires_in} seconds")
|
|
430
|
+
|
|
431
|
+
# Save to cache
|
|
432
|
+
auth_manager._save_token_to_cache()
|
|
433
|
+
logger.info(f"Token successfully saved to cache at {auth_manager._get_token_cache_path()}")
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.error(f"Error saving token to cache: {e}")
|
|
436
|
+
|
|
437
|
+
# Reset auth needed flag
|
|
438
|
+
needs_authentication = False
|
|
439
|
+
|
|
440
|
+
return token_container["token"]
|
|
441
|
+
else:
|
|
442
|
+
logger.warning("Received empty token in callback")
|
|
443
|
+
needs_authentication = True
|
|
444
|
+
return None
|
|
376
445
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
446
|
+
elif self.path.startswith("/confirm-update"):
|
|
447
|
+
# Generate confirmation URL with properly encoded parameters
|
|
448
|
+
query = parse_qs(urlparse(self.path).query)
|
|
449
|
+
adset_id = query.get("adset_id", [""])[0]
|
|
450
|
+
token = query.get("token", [""])[0]
|
|
451
|
+
changes = query.get("changes", ["{}"])[0]
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
changes_dict = json.loads(changes)
|
|
455
|
+
except json.JSONDecodeError:
|
|
456
|
+
changes_dict = {}
|
|
457
|
+
|
|
458
|
+
# Return confirmation page
|
|
459
|
+
self.send_response(200)
|
|
460
|
+
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
461
|
+
self.end_headers()
|
|
380
462
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
463
|
+
html = """
|
|
464
|
+
<html>
|
|
465
|
+
<head>
|
|
466
|
+
<title>Confirm Ad Set Update</title>
|
|
467
|
+
<meta charset="utf-8">
|
|
468
|
+
<style>
|
|
469
|
+
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1000px; margin: 0 auto; }
|
|
470
|
+
.warning { color: #d73a49; margin: 20px 0; padding: 15px; border-left: 4px solid #d73a49; background-color: #fff8f8; }
|
|
471
|
+
.changes { background: #f6f8fa; padding: 15px; border-radius: 6px; }
|
|
472
|
+
.buttons { margin-top: 20px; }
|
|
473
|
+
button { padding: 10px 20px; margin-right: 10px; border-radius: 6px; cursor: pointer; }
|
|
474
|
+
.approve { background: #2ea44f; color: white; border: none; }
|
|
475
|
+
.cancel { background: #d73a49; color: white; border: none; }
|
|
476
|
+
.diff-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
|
477
|
+
.diff-table td { padding: 8px; border: 1px solid #ddd; }
|
|
478
|
+
.diff-table .header { background: #f1f8ff; font-weight: bold; }
|
|
479
|
+
.status { padding: 15px; margin-top: 20px; border-radius: 6px; display: none; }
|
|
480
|
+
.success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
|
|
481
|
+
.error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
|
|
482
|
+
pre { white-space: pre-wrap; word-break: break-all; }
|
|
483
|
+
</style>
|
|
484
|
+
</head>
|
|
485
|
+
<body>
|
|
486
|
+
<h1>Confirm Ad Set Update</h1>
|
|
487
|
+
<p>You are about to update Ad Set: <strong>""" + adset_id + """</strong></p>
|
|
488
|
+
|
|
489
|
+
<div class="warning">
|
|
490
|
+
<p><strong>Warning:</strong> This action will directly update your ad set in Meta Ads. Please review the changes carefully before approving.</p>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div class="changes">
|
|
494
|
+
<h3>Changes to apply:</h3>
|
|
495
|
+
<table class="diff-table">
|
|
496
|
+
<tr class="header">
|
|
497
|
+
<td>Field</td>
|
|
498
|
+
<td>New Value</td>
|
|
499
|
+
<td>Description</td>
|
|
500
|
+
</tr>
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
# Special handling for frequency_control_specs
|
|
504
|
+
for k, v in changes_dict.items():
|
|
505
|
+
description = ""
|
|
506
|
+
if k == "frequency_control_specs" and isinstance(v, list) and len(v) > 0:
|
|
507
|
+
spec = v[0]
|
|
508
|
+
if all(key in spec for key in ["event", "interval_days", "max_frequency"]):
|
|
509
|
+
description = f"Cap to {spec['max_frequency']} {spec['event'].lower()} per {spec['interval_days']} days"
|
|
510
|
+
|
|
511
|
+
# Special handling for targeting_automation
|
|
512
|
+
elif k == "targeting" and isinstance(v, dict) and "targeting_automation" in v:
|
|
513
|
+
targeting_auto = v.get("targeting_automation", {})
|
|
514
|
+
if "advantage_audience" in targeting_auto:
|
|
515
|
+
audience_value = targeting_auto["advantage_audience"]
|
|
516
|
+
description = f"Set Advantage+ audience to {'ON' if audience_value == 1 else 'OFF'}"
|
|
517
|
+
if audience_value == 1:
|
|
518
|
+
description += " (may be restricted for Special Ad Categories)"
|
|
519
|
+
|
|
520
|
+
# Format the value for display
|
|
521
|
+
display_value = json.dumps(v, indent=2) if isinstance(v, (dict, list)) else str(v)
|
|
522
|
+
|
|
523
|
+
html += f"""
|
|
524
|
+
<tr>
|
|
525
|
+
<td>{k}</td>
|
|
526
|
+
<td><pre>{display_value}</pre></td>
|
|
527
|
+
<td>{description}</td>
|
|
528
|
+
</tr>
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
# Create a properly escaped JSON string for JavaScript
|
|
532
|
+
escaped_changes = json.dumps(changes).replace("'", "\\'").replace('"', '\\"')
|
|
385
533
|
|
|
386
|
-
|
|
534
|
+
html += """
|
|
535
|
+
</table>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<div class="buttons">
|
|
539
|
+
<button class="approve" onclick="approveChanges()">Approve Changes</button>
|
|
540
|
+
<button class="cancel" onclick="cancelChanges()">Cancel</button>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div id="status" class="status"></div>
|
|
387
544
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
545
|
+
<script>
|
|
546
|
+
// Enable debug logging
|
|
547
|
+
const DEBUG = true;
|
|
548
|
+
function debugLog(message, data) {
|
|
549
|
+
if (DEBUG) {
|
|
550
|
+
if (data) {
|
|
551
|
+
console.log(`[DEBUG-CONFIRM] ${message}:`, data);
|
|
552
|
+
} else {
|
|
553
|
+
console.log(`[DEBUG-CONFIRM] ${message}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
399
556
|
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function approveChanges() {
|
|
403
|
-
showStatus("Processing update...");
|
|
404
557
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
action: 'approve'
|
|
413
|
-
}))
|
|
414
|
-
.then(response => response.json())
|
|
415
|
-
.then(data => {
|
|
416
|
-
if (data.error) {
|
|
417
|
-
showStatus('Error applying changes: ' + data.error, true);
|
|
418
|
-
buttons.forEach(button => button.disabled = false);
|
|
558
|
+
function showStatus(message, isError = false) {
|
|
559
|
+
const statusElement = document.getElementById('status');
|
|
560
|
+
statusElement.textContent = message;
|
|
561
|
+
statusElement.style.display = 'block';
|
|
562
|
+
if (isError) {
|
|
563
|
+
statusElement.classList.add('error');
|
|
564
|
+
statusElement.classList.remove('success');
|
|
419
565
|
} else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
566
|
+
statusElement.classList.add('success');
|
|
567
|
+
statusElement.classList.remove('error');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function approveChanges() {
|
|
572
|
+
showStatus("Processing update...");
|
|
573
|
+
debugLog("Approving changes");
|
|
574
|
+
|
|
575
|
+
const buttons = document.querySelectorAll('button');
|
|
576
|
+
buttons.forEach(button => button.disabled = true);
|
|
577
|
+
|
|
578
|
+
const params = new URLSearchParams({
|
|
579
|
+
adset_id: '""" + adset_id + """',
|
|
580
|
+
token: '""" + token + """',
|
|
581
|
+
changes: '""" + escaped_changes + """',
|
|
582
|
+
action: 'approve'
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
debugLog("Sending update request with params", {
|
|
586
|
+
adset_id: '""" + adset_id + """',
|
|
587
|
+
changes: JSON.parse('""" + escaped_changes + """')
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
fetch('/update-confirm?' + params)
|
|
591
|
+
.then(response => {
|
|
592
|
+
debugLog("Received response", { status: response.status });
|
|
593
|
+
return response.text().then(text => {
|
|
594
|
+
debugLog("Raw response text", text);
|
|
595
|
+
try {
|
|
596
|
+
return JSON.parse(text);
|
|
597
|
+
} catch (e) {
|
|
598
|
+
debugLog("Error parsing JSON response", e);
|
|
599
|
+
return { status: "error", error: "Invalid response format from server" };
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
})
|
|
603
|
+
.then(data => {
|
|
604
|
+
debugLog("Parsed response data", data);
|
|
605
|
+
|
|
606
|
+
if (data.status === "error") {
|
|
607
|
+
// Build a properly encoded and detailed error message
|
|
608
|
+
let errorMessage = data.error || "Unknown error";
|
|
609
|
+
|
|
610
|
+
// Extract the most appropriate error message for display
|
|
611
|
+
const extractBestErrorMessage = (errorData) => {
|
|
612
|
+
// Try to find the most user-friendly message in the error data
|
|
613
|
+
if (!errorData) return null;
|
|
614
|
+
|
|
615
|
+
// Meta often puts the most user-friendly message in error_user_msg
|
|
616
|
+
if (errorData.apiError && errorData.apiError.error_user_msg) {
|
|
617
|
+
return errorData.apiError.error_user_msg;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Look for detailed error messages in blame_field_specs
|
|
621
|
+
try {
|
|
622
|
+
if (errorData.apiError && errorData.apiError.error_data) {
|
|
623
|
+
const errorDataObj = typeof errorData.apiError.error_data === 'string'
|
|
624
|
+
? JSON.parse(errorData.apiError.error_data)
|
|
625
|
+
: errorData.apiError.error_data;
|
|
626
|
+
|
|
627
|
+
if (errorDataObj.blame_field_specs && errorDataObj.blame_field_specs.length > 0) {
|
|
628
|
+
// Handle nested array structure
|
|
629
|
+
const specs = errorDataObj.blame_field_specs[0];
|
|
630
|
+
if (Array.isArray(specs)) {
|
|
631
|
+
return specs.filter(Boolean).join("; ");
|
|
632
|
+
} else if (typeof specs === 'string') {
|
|
633
|
+
return specs;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch (e) {
|
|
638
|
+
debugLog("Error extracting blame_field_specs", e);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Fall back to standard error message if available
|
|
642
|
+
if (errorData.apiError && errorData.apiError.message) {
|
|
643
|
+
return errorData.apiError.message;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// If we have error details as an array, join them
|
|
647
|
+
if (errorData.details && Array.isArray(errorData.details) && errorData.details.length > 0) {
|
|
648
|
+
return errorData.details.join("; ");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// No better message found
|
|
652
|
+
return null;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Find the best error message
|
|
656
|
+
const bestMessage = extractBestErrorMessage(data);
|
|
657
|
+
if (bestMessage) {
|
|
658
|
+
errorMessage = bestMessage;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
debugLog("Selected error message", errorMessage);
|
|
662
|
+
|
|
663
|
+
// Get the original error from the nested structure if possible
|
|
664
|
+
const originalErrorDetails = data.apiError?.details?.error || data.apiError;
|
|
665
|
+
|
|
666
|
+
// Create a detailed error object for the verification page
|
|
667
|
+
const fullErrorData = {
|
|
668
|
+
message: errorMessage,
|
|
669
|
+
details: data.errorDetails || [],
|
|
670
|
+
apiError: originalErrorDetails || data.apiError || {},
|
|
671
|
+
fullResponse: data.fullResponse || {}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
debugLog("Redirecting with error message", errorMessage);
|
|
675
|
+
debugLog("Full error data", fullErrorData);
|
|
676
|
+
|
|
677
|
+
// Encode the stringified error object
|
|
678
|
+
const encodedErrorData = encodeURIComponent(JSON.stringify(fullErrorData));
|
|
679
|
+
|
|
680
|
+
// Redirect to verification page with detailed error information
|
|
681
|
+
const errorParams = new URLSearchParams({
|
|
423
682
|
adset_id: '""" + adset_id + """',
|
|
424
|
-
token: '""" + token + """'
|
|
683
|
+
token: '""" + token + """',
|
|
684
|
+
error: errorMessage,
|
|
685
|
+
errorData: encodedErrorData
|
|
425
686
|
});
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
687
|
+
window.location.href = '/verify-update?' + errorParams;
|
|
688
|
+
} else {
|
|
689
|
+
showStatus('Changes approved and will be applied shortly!');
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
window.location.href = '/verify-update?' + new URLSearchParams({
|
|
692
|
+
adset_id: '""" + adset_id + """',
|
|
693
|
+
token: '""" + token + """'
|
|
694
|
+
});
|
|
695
|
+
}, 3000);
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
.catch(error => {
|
|
699
|
+
debugLog("Fetch error", error);
|
|
700
|
+
showStatus('Error applying changes: ' + error, true);
|
|
701
|
+
buttons.forEach(button => button.disabled = false);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
437
704
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
query = parse_qs(urlparse(self.path).query)
|
|
457
|
-
adset_id = query.get("adset_id", [""])[0]
|
|
458
|
-
token = query.get("token", [""])[0]
|
|
459
|
-
|
|
460
|
-
# Respond with a verification page
|
|
461
|
-
self.send_response(200)
|
|
462
|
-
self.send_header("Content-type", "text/html")
|
|
463
|
-
self.end_headers()
|
|
705
|
+
function cancelChanges() {
|
|
706
|
+
showStatus("Cancelling update...");
|
|
707
|
+
|
|
708
|
+
fetch('/update-confirm?' + new URLSearchParams({
|
|
709
|
+
adset_id: '""" + adset_id + """',
|
|
710
|
+
action: 'cancel'
|
|
711
|
+
}))
|
|
712
|
+
.then(() => {
|
|
713
|
+
showStatus('Update cancelled.');
|
|
714
|
+
setTimeout(() => window.close(), 2000);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
</script>
|
|
718
|
+
</body>
|
|
719
|
+
</html>
|
|
720
|
+
"""
|
|
721
|
+
self.wfile.write(html.encode('utf-8'))
|
|
722
|
+
return
|
|
464
723
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
.error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
|
|
475
|
-
.details { background: #f6f8fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
|
|
476
|
-
.note { background-color: #fff8c5; border: 1px solid #e36209; padding: 15px; border-radius: 6px; margin: 20px 0; }
|
|
477
|
-
pre { white-space: pre-wrap; word-break: break-all; }
|
|
478
|
-
.spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #0366d6; animation: spin 1s ease-in-out infinite; margin-right: 10px; }
|
|
479
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
480
|
-
</style>
|
|
481
|
-
</head>
|
|
482
|
-
<body>
|
|
483
|
-
<h1>Verifying Ad Set Update</h1>
|
|
484
|
-
<p>Checking the status of your update for Ad Set <strong>""" + adset_id + """</strong></p>
|
|
724
|
+
elif self.path.startswith("/verify-update"):
|
|
725
|
+
# Parse query parameters
|
|
726
|
+
query = parse_qs(urlparse(self.path).query)
|
|
727
|
+
adset_id = query.get("adset_id", [""])[0]
|
|
728
|
+
token = query.get("token", [""])[0]
|
|
729
|
+
|
|
730
|
+
# Check if there was an error in the update process
|
|
731
|
+
error_message = query.get("error", [""])[0]
|
|
732
|
+
error_data_encoded = query.get("errorData", [""])[0]
|
|
485
733
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
734
|
+
# Try to decode detailed error data if available
|
|
735
|
+
error_data = {}
|
|
736
|
+
if error_data_encoded:
|
|
737
|
+
try:
|
|
738
|
+
error_data = json.loads(urllib.parse.unquote(error_data_encoded))
|
|
739
|
+
except:
|
|
740
|
+
logger.error("Failed to parse errorData parameter")
|
|
490
741
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
742
|
+
# Respond with a verification page
|
|
743
|
+
self.send_response(200)
|
|
744
|
+
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
745
|
+
self.end_headers()
|
|
494
746
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
747
|
+
html = """
|
|
748
|
+
<html>
|
|
749
|
+
<head>
|
|
750
|
+
<title>Verifying Ad Set Update</title>
|
|
751
|
+
<meta charset="utf-8">
|
|
752
|
+
<style>
|
|
753
|
+
body { font-family: Arial, sans-serif; margin: 20px; max-width: 800px; margin: 0 auto; }
|
|
754
|
+
.status { padding: 15px; margin-top: 20px; border-radius: 6px; }
|
|
755
|
+
.loading { background-color: #f1f8ff; border: 1px solid #0366d6; }
|
|
756
|
+
.success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
|
|
757
|
+
.error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
|
|
758
|
+
.details { background: #f6f8fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
|
|
759
|
+
.note { background-color: #fff8c5; border: 1px solid #e36209; padding: 15px; border-radius: 6px; margin: 20px 0; }
|
|
760
|
+
pre { white-space: pre-wrap; word-break: break-all; }
|
|
761
|
+
.spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #0366d6; animation: spin 1s ease-in-out infinite; margin-right: 10px; }
|
|
762
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
763
|
+
.fix-suggestion { background-color: #e6f6ff; border: 1px solid #79b8ff; padding: 15px; border-radius: 6px; margin-top: 10px; }
|
|
764
|
+
.code-block { background-color: #f6f8fa; padding: 8px; border-radius: 4px; font-family: monospace; }
|
|
765
|
+
.error-list { margin-top: 10px; }
|
|
766
|
+
.error-list li { margin-bottom: 8px; }
|
|
767
|
+
.debug-section { background-color: #f0f0f0; margin-top: 30px; padding: 15px; border: 1px dashed #666; }
|
|
768
|
+
.debug-section h3 { color: #333; }
|
|
769
|
+
.raw-response { font-family: monospace; font-size: 12px; max-height: 300px; overflow: auto; }
|
|
770
|
+
.error-details { margin-top: 15px; background-color: #fff5f5; padding: 15px; border-left: 3px solid #d73a49; }
|
|
771
|
+
</style>
|
|
772
|
+
</head>
|
|
773
|
+
<body>
|
|
774
|
+
<h1>Verifying Ad Set Update</h1>
|
|
775
|
+
<p>Checking the status of your update for Ad Set <strong>""" + adset_id + """</strong></p>
|
|
776
|
+
|
|
777
|
+
<div class="note">
|
|
778
|
+
<strong>Note about Meta API Visibility:</strong>
|
|
779
|
+
<p>Some fields (like frequency caps) may not be visible in the API response even when successfully set. This is a limitation of the Meta API and depends on factors like the ad set's optimization goal. You can verify these settings in the Meta Ads Manager UI or by monitoring metrics like frequency in the ad insights.</p>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<div id="status" class="status loading">
|
|
783
|
+
<div class="spinner"></div> Verifying update...
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<div id="details" class="details" style="display: none;">
|
|
787
|
+
<h3>Updated Ad Set Details:</h3>
|
|
788
|
+
<pre id="adset-details">Loading...</pre>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<div id="debug-section" class="debug-section" style="display: none;">
|
|
792
|
+
<h3>Debug Information</h3>
|
|
793
|
+
<div>
|
|
794
|
+
<h4>URL Parameters:</h4>
|
|
795
|
+
<pre id="url-params">Loading...</pre>
|
|
796
|
+
</div>
|
|
797
|
+
<div>
|
|
798
|
+
<h4>Error Data:</h4>
|
|
799
|
+
<pre id="error-data-debug">""" + json.dumps(error_data, indent=2) + """</pre>
|
|
800
|
+
</div>
|
|
801
|
+
<div>
|
|
802
|
+
<h4>Raw API Response:</h4>
|
|
803
|
+
<pre id="raw-response" class="raw-response">Loading...</pre>
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
499
806
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
807
|
+
<script>
|
|
808
|
+
// Enable debug mode
|
|
809
|
+
const DEBUG = true;
|
|
810
|
+
|
|
811
|
+
// Debug logging helper
|
|
812
|
+
function debugLog(message, data) {
|
|
813
|
+
if (DEBUG) {
|
|
814
|
+
if (data) {
|
|
815
|
+
console.log(`[DEBUG] ${message}:`, data);
|
|
816
|
+
} else {
|
|
817
|
+
console.log(`[DEBUG] ${message}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Show debug section
|
|
823
|
+
if (DEBUG) {
|
|
824
|
+
document.getElementById('debug-section').style.display = 'block';
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Parse and display URL parameters
|
|
828
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
829
|
+
const urlParamsObj = {};
|
|
830
|
+
for (const [key, value] of urlParams.entries()) {
|
|
831
|
+
urlParamsObj[key] = value;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
debugLog('URL parameters', urlParamsObj);
|
|
835
|
+
document.getElementById('url-params').textContent = JSON.stringify(urlParamsObj, null, 2);
|
|
836
|
+
|
|
837
|
+
// Try to parse error data if available
|
|
838
|
+
let errorData = null;
|
|
839
|
+
const errorDataParam = urlParams.get('errorData');
|
|
840
|
+
if (errorDataParam) {
|
|
841
|
+
try {
|
|
842
|
+
errorData = JSON.parse(decodeURIComponent(errorDataParam));
|
|
843
|
+
debugLog('Parsed error data', errorData);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
debugLog('Failed to parse errorData', e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Check if there was an error passed in the URL
|
|
850
|
+
const errorParam = urlParams.get('error');
|
|
851
|
+
debugLog('Error parameter found', errorParam);
|
|
852
|
+
|
|
853
|
+
if (errorParam) {
|
|
854
|
+
// If there's an error, show it immediately
|
|
855
|
+
const errorMessage = decodeURIComponent(errorParam);
|
|
856
|
+
debugLog('Decoded error message', errorMessage);
|
|
516
857
|
|
|
517
|
-
// Update success message to reflect API visibility limitations
|
|
518
|
-
statusElement.classList.remove('loading');
|
|
519
|
-
statusElement.classList.add('success');
|
|
520
|
-
statusElement.innerHTML = '✅ Update request was processed successfully. Please verify the changes in Meta Ads Manager UI or monitor ad performance metrics.';
|
|
521
|
-
} catch (error) {
|
|
522
858
|
const statusElement = document.getElementById('status');
|
|
523
859
|
statusElement.classList.remove('loading');
|
|
524
860
|
statusElement.classList.add('error');
|
|
525
|
-
|
|
861
|
+
|
|
862
|
+
// Check if this is a Special Ad Category error
|
|
863
|
+
const isSpecialAdCategoryError =
|
|
864
|
+
errorMessage.includes("Special Ad Category") ||
|
|
865
|
+
errorMessage.includes("Advantage+") ||
|
|
866
|
+
errorMessage.includes("advantage_audience");
|
|
867
|
+
|
|
868
|
+
debugLog('Is Special Ad Category error', isSpecialAdCategoryError);
|
|
869
|
+
|
|
870
|
+
if (isSpecialAdCategoryError) {
|
|
871
|
+
// Format special ad category errors with better explanation
|
|
872
|
+
debugLog('Displaying Special Ad Category error');
|
|
873
|
+
statusElement.innerHTML = `
|
|
874
|
+
<h3>❌ Special Ad Category Restriction</h3>
|
|
875
|
+
<p>${errorMessage}</p>
|
|
876
|
+
<div class="note" style="margin-top:10px">
|
|
877
|
+
<strong>What does this mean?</strong><br>
|
|
878
|
+
Meta restricts certain targeting features like Advantage+ audience for ads in Special Ad Categories
|
|
879
|
+
(housing, employment, credit, social issues, etc.). You need to use standard targeting options instead.
|
|
880
|
+
</div>
|
|
881
|
+
<div class="fix-suggestion">
|
|
882
|
+
<strong>How to fix:</strong><br>
|
|
883
|
+
To update this ad set, try setting <span class="code-block">targeting.targeting_automation.advantage_audience</span> to <span class="code-block">0</span> instead of <span class="code-block">1</span>.
|
|
884
|
+
</div>
|
|
885
|
+
`;
|
|
886
|
+
} else {
|
|
887
|
+
// Standard error display with more details
|
|
888
|
+
debugLog('Displaying standard error');
|
|
889
|
+
|
|
890
|
+
// Start with basic error display
|
|
891
|
+
let errorHtml = `
|
|
892
|
+
<h3>❌ Error updating ad set</h3>
|
|
893
|
+
<p>${errorMessage}</p>
|
|
894
|
+
`;
|
|
895
|
+
|
|
896
|
+
// Add detailed error information if available
|
|
897
|
+
if (errorData) {
|
|
898
|
+
errorHtml += `<div class="error-details">`;
|
|
899
|
+
|
|
900
|
+
// If we have a nice error title from Meta, display it
|
|
901
|
+
if (errorData.apiError && errorData.apiError.error_user_title) {
|
|
902
|
+
errorHtml += `<strong>Error Type:</strong> ${errorData.apiError.error_user_title}<br>`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Add error codes if available
|
|
906
|
+
if (errorData.apiError) {
|
|
907
|
+
const apiError = errorData.apiError;
|
|
908
|
+
if (apiError.code) {
|
|
909
|
+
errorHtml += `<strong>Error Code:</strong> ${apiError.code}`;
|
|
910
|
+
if (apiError.error_subcode) {
|
|
911
|
+
errorHtml += ` (Subcode: ${apiError.error_subcode})`;
|
|
912
|
+
}
|
|
913
|
+
errorHtml += `<br>`;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Add detailed error list
|
|
918
|
+
if (errorData.details && errorData.details.length > 0) {
|
|
919
|
+
errorHtml += `
|
|
920
|
+
<strong>Error Details:</strong>
|
|
921
|
+
<ul class="error-list">
|
|
922
|
+
${errorData.details.map(detail => `<li>${detail}</li>`).join('')}
|
|
923
|
+
</ul>
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Try to extract and display blame_field_specs
|
|
928
|
+
try {
|
|
929
|
+
if (errorData.apiError && errorData.apiError.error_data) {
|
|
930
|
+
const error_data = typeof errorData.apiError.error_data === 'string'
|
|
931
|
+
? JSON.parse(errorData.apiError.error_data)
|
|
932
|
+
: errorData.apiError.error_data;
|
|
933
|
+
|
|
934
|
+
if (error_data.blame_field_specs && error_data.blame_field_specs.length > 0) {
|
|
935
|
+
errorHtml += `<strong>Field-Specific Errors:</strong><ul class="error-list">`;
|
|
936
|
+
|
|
937
|
+
// Handle different formats of blame_field_specs
|
|
938
|
+
if (Array.isArray(error_data.blame_field_specs[0])) {
|
|
939
|
+
// Format: [[error1, error2, ...]]
|
|
940
|
+
error_data.blame_field_specs[0].forEach(spec => {
|
|
941
|
+
if (spec) errorHtml += `<li>${spec}</li>`;
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
// Format: [error1, error2, ...]
|
|
945
|
+
error_data.blame_field_specs.forEach(spec => {
|
|
946
|
+
if (spec) errorHtml += `<li>${spec}</li>`;
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
errorHtml += `</ul>`;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
} catch (e) {
|
|
954
|
+
debugLog('Error parsing blame_field_specs', e);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
errorHtml += `</div>`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
statusElement.innerHTML = errorHtml;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Just display current state, don't try to verify the update since it failed
|
|
964
|
+
document.getElementById('details').innerHTML = `
|
|
965
|
+
<h3>Current Ad Set Details (Changes Not Applied):</h3>
|
|
966
|
+
<p>Fetching current state...</p>
|
|
967
|
+
<pre id="adset-details">Loading...</pre>
|
|
968
|
+
`;
|
|
969
|
+
document.getElementById('details').style.display = 'block';
|
|
970
|
+
|
|
971
|
+
// Fetch the current ad set details to show what wasn't changed
|
|
972
|
+
fetchAdSetDetails();
|
|
973
|
+
} else {
|
|
974
|
+
// Otherwise proceed with normal verification
|
|
975
|
+
debugLog('No error parameter found, proceeding with verification');
|
|
976
|
+
setTimeout(verifyUpdate, 3000);
|
|
526
977
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
978
|
+
|
|
979
|
+
// Function to just fetch the ad set details
|
|
980
|
+
async function fetchAdSetDetails() {
|
|
981
|
+
try {
|
|
982
|
+
debugLog('Fetching ad set details');
|
|
983
|
+
const apiUrl = '/api/adset?' + new URLSearchParams({
|
|
984
|
+
adset_id: '""" + adset_id + """',
|
|
985
|
+
token: '""" + token + """'
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
debugLog('Fetching from URL', apiUrl);
|
|
989
|
+
const response = await fetch(apiUrl);
|
|
990
|
+
|
|
991
|
+
// Log raw response
|
|
992
|
+
const responseText = await response.text();
|
|
993
|
+
debugLog('Raw API response text', responseText);
|
|
994
|
+
document.getElementById('raw-response').textContent = responseText;
|
|
995
|
+
|
|
996
|
+
// Parse JSON
|
|
997
|
+
let data;
|
|
998
|
+
try {
|
|
999
|
+
data = JSON.parse(responseText);
|
|
1000
|
+
debugLog('Parsed API response', data);
|
|
1001
|
+
} catch (parseError) {
|
|
1002
|
+
debugLog('Error parsing JSON response', parseError);
|
|
1003
|
+
throw new Error(`Failed to parse API response: ${parseError.message}`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const detailsElement = document.getElementById('details');
|
|
1007
|
+
const adsetDetailsElement = document.getElementById('adset-details');
|
|
1008
|
+
|
|
1009
|
+
// Check if there's an error in the response
|
|
1010
|
+
if (data.error) {
|
|
1011
|
+
debugLog('Error in API response', data.error);
|
|
1012
|
+
adsetDetailsElement.textContent = JSON.stringify({
|
|
1013
|
+
error: "Error fetching ad set details",
|
|
1014
|
+
details: typeof data.error === 'object' ?
|
|
1015
|
+
data.error.message || JSON.stringify(data.error) :
|
|
1016
|
+
data.error
|
|
1017
|
+
}, null, 2);
|
|
1018
|
+
} else {
|
|
1019
|
+
// No errors, display the ad set details
|
|
1020
|
+
debugLog('Successfully fetched ad set details');
|
|
1021
|
+
detailsElement.style.display = 'block';
|
|
1022
|
+
adsetDetailsElement.textContent = JSON.stringify(data, null, 2);
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
debugLog('Error in fetchAdSetDetails', error);
|
|
1026
|
+
console.error('Error fetching ad set details:', error);
|
|
1027
|
+
const adsetDetailsElement = document.getElementById('adset-details');
|
|
1028
|
+
if (adsetDetailsElement) {
|
|
1029
|
+
adsetDetailsElement.textContent = "Error fetching ad set details: " + error.message;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Function to fetch the ad set details and check if frequency_control_specs was updated
|
|
1035
|
+
async function verifyUpdate() {
|
|
1036
|
+
try {
|
|
1037
|
+
debugLog('Starting verification of update');
|
|
1038
|
+
const apiUrl = '/api/adset?' + new URLSearchParams({
|
|
1039
|
+
adset_id: '""" + adset_id + """',
|
|
1040
|
+
token: '""" + token + """'
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
debugLog('Verifying update from URL', apiUrl);
|
|
1044
|
+
const response = await fetch(apiUrl);
|
|
1045
|
+
|
|
1046
|
+
// Log raw response
|
|
1047
|
+
const responseText = await response.text();
|
|
1048
|
+
debugLog('Raw verification response text', responseText);
|
|
1049
|
+
document.getElementById('raw-response').textContent = responseText;
|
|
1050
|
+
|
|
1051
|
+
// Parse JSON
|
|
1052
|
+
let data;
|
|
1053
|
+
try {
|
|
1054
|
+
data = JSON.parse(responseText);
|
|
1055
|
+
debugLog('Parsed verification response', data);
|
|
1056
|
+
} catch (parseError) {
|
|
1057
|
+
debugLog('Error parsing JSON verification response', parseError);
|
|
1058
|
+
throw new Error(`Failed to parse verification response: ${parseError.message}`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const statusElement = document.getElementById('status');
|
|
1062
|
+
const detailsElement = document.getElementById('details');
|
|
1063
|
+
const adsetDetailsElement = document.getElementById('adset-details');
|
|
1064
|
+
|
|
1065
|
+
detailsElement.style.display = 'block';
|
|
1066
|
+
adsetDetailsElement.textContent = JSON.stringify(data, null, 2);
|
|
1067
|
+
|
|
1068
|
+
// Check if there's an error in the response
|
|
1069
|
+
if (data.error) {
|
|
1070
|
+
debugLog('Error in verification response', data.error);
|
|
1071
|
+
statusElement.classList.remove('loading');
|
|
1072
|
+
statusElement.classList.add('error');
|
|
1073
|
+
|
|
1074
|
+
// Extract error message from various possible formats
|
|
1075
|
+
let errorMessage = "Unknown error occurred";
|
|
1076
|
+
|
|
1077
|
+
if (typeof data.error === 'string') {
|
|
1078
|
+
errorMessage = data.error;
|
|
1079
|
+
debugLog('Error is string', errorMessage);
|
|
1080
|
+
} else if (data.error.message) {
|
|
1081
|
+
errorMessage = data.error.message;
|
|
1082
|
+
debugLog('Error has message property', errorMessage);
|
|
1083
|
+
} else if (data.error.error_message) {
|
|
1084
|
+
errorMessage = data.error.error_message;
|
|
1085
|
+
debugLog('Error has error_message property', errorMessage);
|
|
1086
|
+
} else {
|
|
1087
|
+
debugLog('Error format unknown', data.error);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Check if this is a Special Ad Category error
|
|
1091
|
+
if (errorMessage.includes("Special Ad Category") ||
|
|
1092
|
+
errorMessage.includes("Advantage+") ||
|
|
1093
|
+
errorMessage.includes("advantage_audience")) {
|
|
1094
|
+
|
|
1095
|
+
// Format special ad category errors with better explanation
|
|
1096
|
+
debugLog('Displaying Special Ad Category verification error');
|
|
1097
|
+
statusElement.innerHTML = `
|
|
1098
|
+
<h3>❌ Special Ad Category Restriction</h3>
|
|
1099
|
+
<p>${errorMessage}</p>
|
|
1100
|
+
<div class="note" style="margin-top:10px">
|
|
1101
|
+
<strong>What does this mean?</strong><br>
|
|
1102
|
+
Meta restricts certain targeting features like Advantage+ audience for ads in Special Ad Categories
|
|
1103
|
+
(housing, employment, credit, social issues, etc.). You need to use standard targeting options instead.
|
|
1104
|
+
</div>
|
|
1105
|
+
<div class="fix-suggestion">
|
|
1106
|
+
<strong>How to fix:</strong><br>
|
|
1107
|
+
To update this ad set, try setting <span class="code-block">targeting.targeting_automation.advantage_audience</span> to <span class="code-block">0</span> instead of <span class="code-block">1</span>.
|
|
1108
|
+
</div>
|
|
1109
|
+
`;
|
|
1110
|
+
} else {
|
|
1111
|
+
debugLog('Displaying standard verification error');
|
|
1112
|
+
statusElement.innerHTML = `
|
|
1113
|
+
<h3>❌ Error retrieving ad set details</h3>
|
|
1114
|
+
<p>${errorMessage}</p>
|
|
1115
|
+
<div class="raw-error" style="margin-top: 10px;">
|
|
1116
|
+
<strong>Raw Error:</strong>
|
|
1117
|
+
<pre>${JSON.stringify(data.error, null, 2)}</pre>
|
|
1118
|
+
</div>
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
// Update success message to reflect API visibility limitations
|
|
1123
|
+
debugLog('Verification successful');
|
|
1124
|
+
statusElement.classList.remove('loading');
|
|
1125
|
+
statusElement.classList.add('success');
|
|
1126
|
+
statusElement.innerHTML = '✅ Update request was processed successfully. Please verify the changes in Meta Ads Manager UI or monitor ad performance metrics.';
|
|
1127
|
+
}
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
debugLog('Error in verifyUpdate', error);
|
|
1130
|
+
const statusElement = document.getElementById('status');
|
|
1131
|
+
statusElement.classList.remove('loading');
|
|
1132
|
+
statusElement.classList.add('error');
|
|
1133
|
+
statusElement.innerHTML = `
|
|
1134
|
+
<h3>❌ Error verifying update</h3>
|
|
1135
|
+
<p>${error.message}</p>
|
|
1136
|
+
<pre>${error.stack}</pre>
|
|
1137
|
+
`;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
</script>
|
|
1141
|
+
</body>
|
|
1142
|
+
</html>
|
|
1143
|
+
"""
|
|
1144
|
+
self.wfile.write(html.encode('utf-8'))
|
|
1145
|
+
return
|
|
576
1146
|
|
|
577
|
-
|
|
1147
|
+
elif self.path.startswith("/api/adset"):
|
|
1148
|
+
# Parse query parameters
|
|
1149
|
+
query = parse_qs(urlparse(self.path).query)
|
|
578
1150
|
adset_id = query.get("adset_id", [""])[0]
|
|
579
1151
|
token = query.get("token", [""])[0]
|
|
580
|
-
changes = query.get("changes", ["{}"])[0]
|
|
581
|
-
|
|
582
|
-
# Store the approval in a global variable for the main thread to process
|
|
583
|
-
global update_confirmation
|
|
584
|
-
update_confirmation = {
|
|
585
|
-
"approved": True,
|
|
586
|
-
"adset_id": adset_id,
|
|
587
|
-
"token": token,
|
|
588
|
-
"changes": changes
|
|
589
|
-
}
|
|
590
1152
|
|
|
591
|
-
# Prepare the API call to actually execute the update
|
|
592
1153
|
from .api import make_api_request
|
|
593
1154
|
|
|
594
|
-
#
|
|
595
|
-
async def
|
|
1155
|
+
# Call the Graph API directly
|
|
1156
|
+
async def get_adset_data():
|
|
596
1157
|
try:
|
|
597
|
-
changes_dict = json.loads(changes)
|
|
598
1158
|
endpoint = f"{adset_id}"
|
|
1159
|
+
params = {
|
|
1160
|
+
"fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,frequency_control_specs"
|
|
1161
|
+
}
|
|
599
1162
|
|
|
600
|
-
|
|
601
|
-
api_params = dict(changes_dict)
|
|
602
|
-
api_params["access_token"] = token
|
|
1163
|
+
result = await make_api_request(endpoint, token, params)
|
|
603
1164
|
|
|
604
|
-
#
|
|
605
|
-
|
|
606
|
-
|
|
1165
|
+
# Check if result is a string (possibly an error message)
|
|
1166
|
+
if isinstance(result, str):
|
|
1167
|
+
try:
|
|
1168
|
+
# Try to parse as JSON
|
|
1169
|
+
parsed_result = json.loads(result)
|
|
1170
|
+
return parsed_result
|
|
1171
|
+
except json.JSONDecodeError:
|
|
1172
|
+
# Return error object if can't parse as JSON
|
|
1173
|
+
return {"error": {"message": result}}
|
|
1174
|
+
|
|
1175
|
+
# If the result is None, return an error object
|
|
1176
|
+
if result is None:
|
|
1177
|
+
return {"error": {"message": "Empty response from API"}}
|
|
1178
|
+
|
|
1179
|
+
return result
|
|
607
1180
|
except Exception as e:
|
|
608
|
-
|
|
1181
|
+
logger.error(f"Error in get_adset_data: {str(e)}")
|
|
1182
|
+
return {"error": {"message": f"Error fetching ad set data: {str(e)}"}}
|
|
609
1183
|
|
|
610
1184
|
# Run the async function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
loop.close()
|
|
616
|
-
|
|
617
|
-
self.wfile.write(json.dumps(result).encode())
|
|
618
|
-
except Exception as e:
|
|
619
|
-
self.wfile.write(json.dumps({"status": "error", "error": str(e)}).encode())
|
|
620
|
-
else:
|
|
621
|
-
# Store the cancellation
|
|
622
|
-
update_confirmation = {
|
|
623
|
-
"approved": False
|
|
624
|
-
}
|
|
625
|
-
self.wfile.write(json.dumps({"status": "cancelled"}).encode())
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
if self.path.startswith("/token"):
|
|
629
|
-
# Extract token from query params
|
|
630
|
-
query = parse_qs(urlparse(self.path).query)
|
|
631
|
-
token_container["token"] = query.get("token", [""])[0]
|
|
632
|
-
|
|
633
|
-
if "expires_in" in query:
|
|
634
|
-
try:
|
|
635
|
-
token_container["expires_in"] = int(query.get("expires_in", ["0"])[0])
|
|
636
|
-
except ValueError:
|
|
637
|
-
token_container["expires_in"] = None
|
|
638
|
-
|
|
639
|
-
# Send success response
|
|
640
|
-
self.send_response(200)
|
|
641
|
-
self.send_header("Content-type", "text/plain")
|
|
642
|
-
self.end_headers()
|
|
643
|
-
self.wfile.write(b"Token received")
|
|
644
|
-
|
|
645
|
-
# Process the token (save it) immediately
|
|
646
|
-
if token_container["token"]:
|
|
647
|
-
# Create token info and save to cache
|
|
648
|
-
logger.info("Token received in callback handler, attempting to save to cache")
|
|
649
|
-
token_info = TokenInfo(
|
|
650
|
-
access_token=token_container["token"],
|
|
651
|
-
expires_in=token_container["expires_in"]
|
|
652
|
-
)
|
|
1185
|
+
loop = asyncio.new_event_loop()
|
|
1186
|
+
asyncio.set_event_loop(loop)
|
|
1187
|
+
result = loop.run_until_complete(get_adset_data())
|
|
1188
|
+
loop.close()
|
|
653
1189
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1190
|
+
# Return the result
|
|
1191
|
+
self.send_response(200)
|
|
1192
|
+
self.send_header("Content-type", "application/json")
|
|
1193
|
+
self.end_headers()
|
|
1194
|
+
self.wfile.write(json.dumps(result, indent=2).encode())
|
|
1195
|
+
return
|
|
1196
|
+
|
|
1197
|
+
elif self.path.startswith("/update-confirm"):
|
|
1198
|
+
# Handle update confirmation response
|
|
1199
|
+
query = parse_qs(urlparse(self.path).query)
|
|
1200
|
+
action = query.get("action", [""])[0]
|
|
665
1201
|
|
|
666
|
-
|
|
667
|
-
|
|
1202
|
+
self.send_response(200)
|
|
1203
|
+
self.send_header("Content-type", "application/json")
|
|
1204
|
+
self.end_headers()
|
|
668
1205
|
|
|
669
|
-
|
|
1206
|
+
if action == "approve":
|
|
1207
|
+
adset_id = query.get("adset_id", [""])[0]
|
|
1208
|
+
token = query.get("token", [""])[0]
|
|
1209
|
+
changes = query.get("changes", ["{}"])[0]
|
|
1210
|
+
|
|
1211
|
+
# Store the approval in a global variable for the main thread to process
|
|
1212
|
+
global update_confirmation
|
|
1213
|
+
update_confirmation = {
|
|
1214
|
+
"approved": True,
|
|
1215
|
+
"adset_id": adset_id,
|
|
1216
|
+
"token": token,
|
|
1217
|
+
"changes": changes
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
# Prepare the API call to actually execute the update
|
|
1221
|
+
from .api import make_api_request
|
|
1222
|
+
|
|
1223
|
+
# Function to perform the actual update
|
|
1224
|
+
async def perform_update():
|
|
1225
|
+
try:
|
|
1226
|
+
# Handle potential multiple encoding of JSON
|
|
1227
|
+
decoded_changes = changes
|
|
1228
|
+
# Try multiple decode attempts to handle various encoding scenarios
|
|
1229
|
+
for _ in range(3): # Try up to 3 levels of decoding
|
|
1230
|
+
try:
|
|
1231
|
+
# Try to parse as JSON
|
|
1232
|
+
changes_obj = json.loads(decoded_changes)
|
|
1233
|
+
# If we got a string back, we need to decode again
|
|
1234
|
+
if isinstance(changes_obj, str):
|
|
1235
|
+
decoded_changes = changes_obj
|
|
1236
|
+
continue
|
|
1237
|
+
else:
|
|
1238
|
+
# We have a dictionary, break the loop
|
|
1239
|
+
changes_dict = changes_obj
|
|
1240
|
+
break
|
|
1241
|
+
except json.JSONDecodeError:
|
|
1242
|
+
# Try unescaping first
|
|
1243
|
+
import html
|
|
1244
|
+
decoded_changes = html.unescape(decoded_changes)
|
|
1245
|
+
try:
|
|
1246
|
+
changes_dict = json.loads(decoded_changes)
|
|
1247
|
+
break
|
|
1248
|
+
except:
|
|
1249
|
+
# Failed to parse, will try again in the next iteration
|
|
1250
|
+
pass
|
|
1251
|
+
else:
|
|
1252
|
+
# If we got here, we couldn't parse the JSON
|
|
1253
|
+
return {"status": "error", "error": f"Failed to decode changes JSON: {changes}"}
|
|
1254
|
+
|
|
1255
|
+
endpoint = f"{adset_id}"
|
|
1256
|
+
|
|
1257
|
+
# Create API parameters properly
|
|
1258
|
+
api_params = {}
|
|
1259
|
+
|
|
1260
|
+
# Add each change parameter
|
|
1261
|
+
for key, value in changes_dict.items():
|
|
1262
|
+
api_params[key] = value
|
|
1263
|
+
|
|
1264
|
+
# Add the access token
|
|
1265
|
+
api_params["access_token"] = token
|
|
1266
|
+
|
|
1267
|
+
# Log what we're about to send
|
|
1268
|
+
logger.info(f"Sending update to Meta API for ad set {adset_id}")
|
|
1269
|
+
logger.info(f"Parameters: {json.dumps(api_params)}")
|
|
1270
|
+
|
|
1271
|
+
# Make the API request to update the ad set
|
|
1272
|
+
result = await make_api_request(endpoint, token, api_params, method="POST")
|
|
1273
|
+
|
|
1274
|
+
# Log the result
|
|
1275
|
+
logger.info(f"Meta API update result: {json.dumps(result) if isinstance(result, dict) else result}")
|
|
1276
|
+
|
|
1277
|
+
# Handle various result formats
|
|
1278
|
+
if result is None:
|
|
1279
|
+
logger.error("Empty response from Meta API")
|
|
1280
|
+
return {"status": "error", "error": "Empty response from Meta API"}
|
|
1281
|
+
|
|
1282
|
+
# Check if the result contains an error
|
|
1283
|
+
if isinstance(result, dict) and 'error' in result:
|
|
1284
|
+
# Extract detailed error message from Meta API
|
|
1285
|
+
error_obj = result['error']
|
|
1286
|
+
error_msg = error_obj.get('message', 'Unknown API error')
|
|
1287
|
+
detailed_error = ""
|
|
1288
|
+
|
|
1289
|
+
# Check for more detailed error messages
|
|
1290
|
+
if 'error_user_msg' in error_obj and error_obj['error_user_msg']:
|
|
1291
|
+
detailed_error = error_obj['error_user_msg']
|
|
1292
|
+
logger.error(f"Meta API user-facing error message: {detailed_error}")
|
|
1293
|
+
|
|
1294
|
+
# Extract error data if available
|
|
1295
|
+
error_specs = []
|
|
1296
|
+
if 'error_data' in error_obj and isinstance(error_obj['error_data'], str):
|
|
1297
|
+
try:
|
|
1298
|
+
error_data = json.loads(error_obj['error_data'])
|
|
1299
|
+
if 'blame_field_specs' in error_data and error_data['blame_field_specs']:
|
|
1300
|
+
blame_specs = error_data['blame_field_specs']
|
|
1301
|
+
if isinstance(blame_specs, list) and blame_specs:
|
|
1302
|
+
if isinstance(blame_specs[0], list):
|
|
1303
|
+
error_specs = [msg for msg in blame_specs[0] if msg]
|
|
1304
|
+
else:
|
|
1305
|
+
error_specs = [str(spec) for spec in blame_specs if spec]
|
|
1306
|
+
|
|
1307
|
+
if error_specs:
|
|
1308
|
+
logger.error(f"Meta API blame field specs: {'; '.join(error_specs)}")
|
|
1309
|
+
except Exception as e:
|
|
1310
|
+
logger.error(f"Error parsing error_data: {e}")
|
|
1311
|
+
|
|
1312
|
+
# Construct most descriptive error message
|
|
1313
|
+
if detailed_error:
|
|
1314
|
+
error_msg = detailed_error
|
|
1315
|
+
elif error_specs:
|
|
1316
|
+
error_msg = "; ".join(error_specs)
|
|
1317
|
+
|
|
1318
|
+
# Log the detailed error information
|
|
1319
|
+
logger.error(f"Meta API error: {error_msg}")
|
|
1320
|
+
logger.error(f"Full error object: {json.dumps(error_obj)}")
|
|
1321
|
+
|
|
1322
|
+
return {
|
|
1323
|
+
"status": "error",
|
|
1324
|
+
"error": error_msg,
|
|
1325
|
+
"api_error": result['error'],
|
|
1326
|
+
"detailed_errors": error_specs,
|
|
1327
|
+
"full_response": result
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
# Handle string results (which might be error messages)
|
|
1331
|
+
if isinstance(result, str):
|
|
1332
|
+
try:
|
|
1333
|
+
# Try to parse as JSON
|
|
1334
|
+
result_obj = json.loads(result)
|
|
1335
|
+
if isinstance(result_obj, dict) and 'error' in result_obj:
|
|
1336
|
+
return {"status": "error", "error": result_obj['error'].get('message', 'Unknown API error')}
|
|
1337
|
+
except:
|
|
1338
|
+
# If not parseable as JSON, return as error message
|
|
1339
|
+
return {"status": "error", "error": result}
|
|
1340
|
+
|
|
1341
|
+
# If we got here, assume success
|
|
1342
|
+
return {"status": "approved", "api_result": result}
|
|
1343
|
+
except Exception as e:
|
|
1344
|
+
# Log the exception for debugging
|
|
1345
|
+
logger.error(f"Error in perform_update: {str(e)}")
|
|
1346
|
+
import traceback
|
|
1347
|
+
logger.error(traceback.format_exc())
|
|
1348
|
+
return {"status": "error", "error": str(e)}
|
|
1349
|
+
|
|
1350
|
+
# Run the async function
|
|
1351
|
+
try:
|
|
1352
|
+
loop = asyncio.new_event_loop()
|
|
1353
|
+
asyncio.set_event_loop(loop)
|
|
1354
|
+
result = loop.run_until_complete(perform_update())
|
|
1355
|
+
loop.close()
|
|
1356
|
+
|
|
1357
|
+
# Ensure result is a dictionary
|
|
1358
|
+
if not isinstance(result, dict):
|
|
1359
|
+
logger.error(f"Unexpected result type: {type(result)}")
|
|
1360
|
+
self.wfile.write(json.dumps({"status": "error", "error": str(result)}).encode())
|
|
1361
|
+
return
|
|
1362
|
+
|
|
1363
|
+
# Check if the API call returned an error
|
|
1364
|
+
if result.get("status") == "error":
|
|
1365
|
+
error_message = result.get("error", "Unknown error")
|
|
1366
|
+
detailed_errors = result.get("detailed_errors", [])
|
|
1367
|
+
|
|
1368
|
+
# Log the detailed error
|
|
1369
|
+
logger.error(f"Meta API error during ad set update: {error_message}")
|
|
1370
|
+
if "api_error" in result:
|
|
1371
|
+
logger.error(f"Detailed API error: {json.dumps(result['api_error'])}")
|
|
1372
|
+
|
|
1373
|
+
# Prepare error response with all available details
|
|
1374
|
+
error_response = {
|
|
1375
|
+
"status": "error",
|
|
1376
|
+
"error": error_message,
|
|
1377
|
+
"errorDetails": detailed_errors
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
# Include the full API error object for complete details
|
|
1381
|
+
if "api_error" in result:
|
|
1382
|
+
error_response["apiError"] = result["api_error"]
|
|
1383
|
+
|
|
1384
|
+
# Include the full response if available
|
|
1385
|
+
if "full_response" in result:
|
|
1386
|
+
error_response["fullResponse"] = result["full_response"]
|
|
1387
|
+
|
|
1388
|
+
logger.info(f"Returning error response: {json.dumps(error_response)}")
|
|
1389
|
+
self.wfile.write(json.dumps(error_response).encode())
|
|
1390
|
+
else:
|
|
1391
|
+
logger.info("Update successful, returning result")
|
|
1392
|
+
self.wfile.write(json.dumps(result).encode())
|
|
1393
|
+
except Exception as e:
|
|
1394
|
+
logger.error(f"Exception in update-confirm handler: {str(e)}")
|
|
1395
|
+
import traceback
|
|
1396
|
+
logger.error(traceback.format_exc())
|
|
1397
|
+
self.wfile.write(json.dumps({
|
|
1398
|
+
"status": "error",
|
|
1399
|
+
"error": str(e),
|
|
1400
|
+
"traceback": traceback.format_exc()
|
|
1401
|
+
}).encode())
|
|
1402
|
+
else:
|
|
1403
|
+
# Store the cancellation
|
|
1404
|
+
update_confirmation = {
|
|
1405
|
+
"approved": False
|
|
1406
|
+
}
|
|
1407
|
+
self.wfile.write(json.dumps({"status": "cancelled"}).encode())
|
|
1408
|
+
return
|
|
1409
|
+
|
|
670
1410
|
else:
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1411
|
+
# If no matching path, return a 404 error
|
|
1412
|
+
self.send_response(404)
|
|
1413
|
+
self.end_headers()
|
|
1414
|
+
except Exception as e:
|
|
1415
|
+
print(f"Error processing request: {e}")
|
|
1416
|
+
self.send_response(500)
|
|
1417
|
+
self.end_headers()
|
|
674
1418
|
|
|
675
1419
|
# Silence server logs
|
|
676
1420
|
def log_message(self, format, *args):
|
|
@@ -761,37 +1505,123 @@ def start_callback_server():
|
|
|
761
1505
|
|
|
762
1506
|
def process_token_response(token_container):
|
|
763
1507
|
"""Process the token response from Facebook."""
|
|
764
|
-
global needs_authentication
|
|
1508
|
+
global needs_authentication, auth_manager
|
|
765
1509
|
|
|
766
1510
|
if token_container and token_container.get('token'):
|
|
767
1511
|
logger.info("Processing token response from Facebook OAuth")
|
|
768
|
-
token_info = TokenInfo(
|
|
769
|
-
access_token=token_container['token'],
|
|
770
|
-
expires_in=token_container.get('expires_in', 0)
|
|
771
|
-
)
|
|
772
1512
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
logger.
|
|
1513
|
+
# Exchange the short-lived token for a long-lived token
|
|
1514
|
+
short_lived_token = token_container['token']
|
|
1515
|
+
long_lived_token_info = exchange_token_for_long_lived(short_lived_token)
|
|
1516
|
+
|
|
1517
|
+
if long_lived_token_info:
|
|
1518
|
+
logger.info(f"Successfully exchanged for long-lived token (expires in {long_lived_token_info.expires_in} seconds)")
|
|
779
1519
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1520
|
+
try:
|
|
1521
|
+
auth_manager.token_info = long_lived_token_info
|
|
1522
|
+
logger.info(f"Long-lived token info set in auth_manager, expires in {long_lived_token_info.expires_in} seconds")
|
|
1523
|
+
except NameError:
|
|
1524
|
+
logger.error("auth_manager not defined when trying to process token")
|
|
1525
|
+
|
|
1526
|
+
try:
|
|
1527
|
+
logger.info("Attempting to save long-lived token to cache")
|
|
1528
|
+
auth_manager._save_token_to_cache()
|
|
1529
|
+
logger.info(f"Long-lived token successfully saved to cache at {auth_manager._get_token_cache_path()}")
|
|
1530
|
+
except Exception as e:
|
|
1531
|
+
logger.error(f"Error saving token to cache: {e}")
|
|
1532
|
+
|
|
1533
|
+
needs_authentication = False
|
|
1534
|
+
return True
|
|
1535
|
+
else:
|
|
1536
|
+
# Fall back to the short-lived token if exchange fails
|
|
1537
|
+
logger.warning("Failed to exchange for long-lived token, using short-lived token instead")
|
|
1538
|
+
token_info = TokenInfo(
|
|
1539
|
+
access_token=short_lived_token,
|
|
1540
|
+
expires_in=token_container.get('expires_in', 0)
|
|
1541
|
+
)
|
|
786
1542
|
|
|
787
|
-
|
|
788
|
-
|
|
1543
|
+
try:
|
|
1544
|
+
auth_manager.token_info = token_info
|
|
1545
|
+
logger.info(f"Short-lived token info set in auth_manager, expires in {token_info.expires_in} seconds")
|
|
1546
|
+
except NameError:
|
|
1547
|
+
logger.error("auth_manager not defined when trying to process token")
|
|
1548
|
+
|
|
1549
|
+
try:
|
|
1550
|
+
logger.info("Attempting to save token to cache")
|
|
1551
|
+
auth_manager._save_token_to_cache()
|
|
1552
|
+
logger.info(f"Token successfully saved to cache at {auth_manager._get_token_cache_path()}")
|
|
1553
|
+
except Exception as e:
|
|
1554
|
+
logger.error(f"Error saving token to cache: {e}")
|
|
1555
|
+
|
|
1556
|
+
needs_authentication = False
|
|
1557
|
+
return True
|
|
789
1558
|
else:
|
|
790
1559
|
logger.warning("Received empty token in process_token_response")
|
|
791
1560
|
needs_authentication = True
|
|
792
1561
|
return False
|
|
793
1562
|
|
|
794
1563
|
|
|
1564
|
+
def exchange_token_for_long_lived(short_lived_token):
|
|
1565
|
+
"""
|
|
1566
|
+
Exchange a short-lived token for a long-lived token (60 days validity).
|
|
1567
|
+
|
|
1568
|
+
Args:
|
|
1569
|
+
short_lived_token: The short-lived access token received from OAuth flow
|
|
1570
|
+
|
|
1571
|
+
Returns:
|
|
1572
|
+
TokenInfo object with the long-lived token, or None if exchange failed
|
|
1573
|
+
"""
|
|
1574
|
+
logger.info("Attempting to exchange short-lived token for long-lived token")
|
|
1575
|
+
|
|
1576
|
+
try:
|
|
1577
|
+
# Get the app ID from the configuration
|
|
1578
|
+
app_id = meta_config.get_app_id()
|
|
1579
|
+
|
|
1580
|
+
# Get the app secret - this should be securely stored
|
|
1581
|
+
app_secret = os.environ.get("META_APP_SECRET", "")
|
|
1582
|
+
|
|
1583
|
+
if not app_id or not app_secret:
|
|
1584
|
+
logger.error("Missing app_id or app_secret for token exchange")
|
|
1585
|
+
return None
|
|
1586
|
+
|
|
1587
|
+
# Make the API request to exchange the token
|
|
1588
|
+
url = "https://graph.facebook.com/v18.0/oauth/access_token"
|
|
1589
|
+
params = {
|
|
1590
|
+
"grant_type": "fb_exchange_token",
|
|
1591
|
+
"client_id": app_id,
|
|
1592
|
+
"client_secret": app_secret,
|
|
1593
|
+
"fb_exchange_token": short_lived_token
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
logger.debug(f"Making token exchange request to {url}")
|
|
1597
|
+
response = requests.get(url, params=params)
|
|
1598
|
+
|
|
1599
|
+
if response.status_code == 200:
|
|
1600
|
+
data = response.json()
|
|
1601
|
+
logger.debug(f"Token exchange response: {data}")
|
|
1602
|
+
|
|
1603
|
+
# Create TokenInfo from the response
|
|
1604
|
+
# The response includes access_token and expires_in (in seconds)
|
|
1605
|
+
new_token = data.get("access_token")
|
|
1606
|
+
expires_in = data.get("expires_in")
|
|
1607
|
+
|
|
1608
|
+
if new_token:
|
|
1609
|
+
logger.info(f"Received long-lived token, expires in {expires_in} seconds (~{expires_in//86400} days)")
|
|
1610
|
+
return TokenInfo(
|
|
1611
|
+
access_token=new_token,
|
|
1612
|
+
expires_in=expires_in
|
|
1613
|
+
)
|
|
1614
|
+
else:
|
|
1615
|
+
logger.error("No access_token in exchange response")
|
|
1616
|
+
return None
|
|
1617
|
+
else:
|
|
1618
|
+
logger.error(f"Token exchange failed with status {response.status_code}: {response.text}")
|
|
1619
|
+
return None
|
|
1620
|
+
except Exception as e:
|
|
1621
|
+
logger.error(f"Error exchanging token: {e}")
|
|
1622
|
+
return None
|
|
1623
|
+
|
|
1624
|
+
|
|
795
1625
|
async def get_current_access_token() -> Optional[str]:
|
|
796
1626
|
"""Get the current access token from auth manager"""
|
|
797
1627
|
# Use the singleton auth manager
|