meta-ads-mcp 0.2.5__py3-none-any.whl → 0.2.6__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 +3 -1
- meta_ads_mcp/core/ads.py +61 -1
- meta_ads_mcp/core/adsets.py +1 -1
- meta_ads_mcp/core/auth.py +10 -1257
- meta_ads_mcp/core/callback_server.py +958 -0
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.6.dist-info}/METADATA +25 -4
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.6.dist-info}/RECORD +9 -8
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.6.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.6.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/core/auth.py
CHANGED
|
@@ -5,16 +5,19 @@ import time
|
|
|
5
5
|
import platform
|
|
6
6
|
import pathlib
|
|
7
7
|
import os
|
|
8
|
-
import threading
|
|
9
|
-
import socket
|
|
10
8
|
import webbrowser
|
|
11
9
|
import asyncio
|
|
12
|
-
from urllib.parse import urlparse, parse_qs
|
|
13
|
-
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
14
10
|
import json
|
|
15
11
|
from .utils import logger
|
|
16
12
|
import requests
|
|
17
13
|
|
|
14
|
+
# Import from the new callback server module
|
|
15
|
+
from .callback_server import (
|
|
16
|
+
start_callback_server,
|
|
17
|
+
token_container,
|
|
18
|
+
update_confirmation
|
|
19
|
+
)
|
|
20
|
+
|
|
18
21
|
# Auth constants
|
|
19
22
|
AUTH_SCOPE = "ads_management,ads_read,business_management"
|
|
20
23
|
AUTH_REDIRECT_URI = "http://localhost:8888/callback"
|
|
@@ -25,21 +28,9 @@ logger.info("Authentication module initialized")
|
|
|
25
28
|
logger.info(f"Auth scope: {AUTH_SCOPE}")
|
|
26
29
|
logger.info(f"Default redirect URI: {AUTH_REDIRECT_URI}")
|
|
27
30
|
|
|
28
|
-
# Global token container for communication between threads
|
|
29
|
-
token_container = {"token": None, "expires_in": None, "user_id": None}
|
|
30
|
-
|
|
31
|
-
# Global container for update confirmations
|
|
32
|
-
update_confirmation = {"approved": False}
|
|
33
|
-
|
|
34
31
|
# Global flag for authentication state
|
|
35
32
|
needs_authentication = False
|
|
36
33
|
|
|
37
|
-
# Global variable for server thread and state
|
|
38
|
-
callback_server_thread = None
|
|
39
|
-
callback_server_lock = threading.Lock()
|
|
40
|
-
callback_server_running = False
|
|
41
|
-
callback_server_port = None
|
|
42
|
-
|
|
43
34
|
# Meta configuration singleton
|
|
44
35
|
class MetaConfig:
|
|
45
36
|
_instance = None
|
|
@@ -214,6 +205,9 @@ class AuthManager:
|
|
|
214
205
|
# Start the callback server if not already running
|
|
215
206
|
port = start_callback_server()
|
|
216
207
|
|
|
208
|
+
# Update redirect URI with the actual port
|
|
209
|
+
self.redirect_uri = f"http://localhost:{port}/callback"
|
|
210
|
+
|
|
217
211
|
# Generate the auth URL
|
|
218
212
|
auth_url = self.get_auth_url()
|
|
219
213
|
|
|
@@ -262,1247 +256,6 @@ class AuthManager:
|
|
|
262
256
|
self.invalidate_token()
|
|
263
257
|
|
|
264
258
|
|
|
265
|
-
# Callback Handler class definition
|
|
266
|
-
class CallbackHandler(BaseHTTPRequestHandler):
|
|
267
|
-
def do_GET(self):
|
|
268
|
-
global token_container, auth_manager, needs_authentication, update_confirmation
|
|
269
|
-
|
|
270
|
-
try:
|
|
271
|
-
# Print path for debugging
|
|
272
|
-
print(f"Callback server received request: {self.path}")
|
|
273
|
-
|
|
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>
|
|
323
|
-
|
|
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
|
|
378
|
-
|
|
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]
|
|
383
|
-
|
|
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
|
|
389
|
-
|
|
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")
|
|
395
|
-
|
|
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
|
|
445
|
-
|
|
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()
|
|
462
|
-
|
|
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('"', '\\"')
|
|
533
|
-
|
|
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>
|
|
544
|
-
|
|
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
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
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');
|
|
565
|
-
} else {
|
|
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({
|
|
682
|
-
adset_id: '""" + adset_id + """',
|
|
683
|
-
token: '""" + token + """',
|
|
684
|
-
error: errorMessage,
|
|
685
|
-
errorData: encodedErrorData
|
|
686
|
-
});
|
|
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
|
-
}
|
|
704
|
-
|
|
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
|
|
723
|
-
|
|
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]
|
|
733
|
-
|
|
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")
|
|
741
|
-
|
|
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()
|
|
746
|
-
|
|
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>
|
|
806
|
-
|
|
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);
|
|
857
|
-
|
|
858
|
-
const statusElement = document.getElementById('status');
|
|
859
|
-
statusElement.classList.remove('loading');
|
|
860
|
-
statusElement.classList.add('error');
|
|
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);
|
|
977
|
-
}
|
|
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
|
|
1146
|
-
|
|
1147
|
-
elif self.path.startswith("/api/adset"):
|
|
1148
|
-
# Parse query parameters
|
|
1149
|
-
query = parse_qs(urlparse(self.path).query)
|
|
1150
|
-
adset_id = query.get("adset_id", [""])[0]
|
|
1151
|
-
token = query.get("token", [""])[0]
|
|
1152
|
-
|
|
1153
|
-
from .api import make_api_request
|
|
1154
|
-
|
|
1155
|
-
# Call the Graph API directly
|
|
1156
|
-
async def get_adset_data():
|
|
1157
|
-
try:
|
|
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
|
-
}
|
|
1162
|
-
|
|
1163
|
-
result = await make_api_request(endpoint, token, params)
|
|
1164
|
-
|
|
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
|
|
1180
|
-
except Exception as e:
|
|
1181
|
-
logger.error(f"Error in get_adset_data: {str(e)}")
|
|
1182
|
-
return {"error": {"message": f"Error fetching ad set data: {str(e)}"}}
|
|
1183
|
-
|
|
1184
|
-
# Run the async function
|
|
1185
|
-
loop = asyncio.new_event_loop()
|
|
1186
|
-
asyncio.set_event_loop(loop)
|
|
1187
|
-
result = loop.run_until_complete(get_adset_data())
|
|
1188
|
-
loop.close()
|
|
1189
|
-
|
|
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]
|
|
1201
|
-
|
|
1202
|
-
self.send_response(200)
|
|
1203
|
-
self.send_header("Content-type", "application/json")
|
|
1204
|
-
self.end_headers()
|
|
1205
|
-
|
|
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
|
-
|
|
1410
|
-
else:
|
|
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()
|
|
1418
|
-
|
|
1419
|
-
# Silence server logs
|
|
1420
|
-
def log_message(self, format, *args):
|
|
1421
|
-
return
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
def start_callback_server():
|
|
1425
|
-
"""Start the callback server if it's not already running"""
|
|
1426
|
-
global callback_server_thread, callback_server_running, callback_server_port, auth_manager
|
|
1427
|
-
|
|
1428
|
-
with callback_server_lock:
|
|
1429
|
-
if callback_server_running:
|
|
1430
|
-
print(f"Callback server already running on port {callback_server_port}")
|
|
1431
|
-
return callback_server_port
|
|
1432
|
-
|
|
1433
|
-
# Find an available port
|
|
1434
|
-
port = 8888
|
|
1435
|
-
max_attempts = 10
|
|
1436
|
-
for attempt in range(max_attempts):
|
|
1437
|
-
try:
|
|
1438
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1439
|
-
s.bind(('localhost', port))
|
|
1440
|
-
break
|
|
1441
|
-
except OSError:
|
|
1442
|
-
port += 1
|
|
1443
|
-
if attempt == max_attempts - 1:
|
|
1444
|
-
raise Exception(f"Could not find an available port after {max_attempts} attempts")
|
|
1445
|
-
|
|
1446
|
-
# Update auth manager's redirect URI with new port
|
|
1447
|
-
if 'auth_manager' in globals():
|
|
1448
|
-
auth_manager.redirect_uri = f"http://localhost:{port}/callback"
|
|
1449
|
-
callback_server_port = port
|
|
1450
|
-
|
|
1451
|
-
try:
|
|
1452
|
-
# Get the CallbackHandler class from global scope
|
|
1453
|
-
handler_class = globals()['CallbackHandler']
|
|
1454
|
-
|
|
1455
|
-
# Create and start server in a daemon thread
|
|
1456
|
-
server = HTTPServer(('localhost', port), handler_class)
|
|
1457
|
-
print(f"Callback server starting on port {port}")
|
|
1458
|
-
|
|
1459
|
-
# Create a simple flag to signal when the server is ready
|
|
1460
|
-
server_ready = threading.Event()
|
|
1461
|
-
|
|
1462
|
-
def server_thread():
|
|
1463
|
-
try:
|
|
1464
|
-
# Signal that the server thread has started
|
|
1465
|
-
server_ready.set()
|
|
1466
|
-
print(f"Callback server is now ready on port {port}")
|
|
1467
|
-
# Start serving HTTP requests
|
|
1468
|
-
server.serve_forever()
|
|
1469
|
-
except Exception as e:
|
|
1470
|
-
print(f"Server error: {e}")
|
|
1471
|
-
finally:
|
|
1472
|
-
with callback_server_lock:
|
|
1473
|
-
global callback_server_running
|
|
1474
|
-
callback_server_running = False
|
|
1475
|
-
|
|
1476
|
-
callback_server_thread = threading.Thread(target=server_thread)
|
|
1477
|
-
callback_server_thread.daemon = True
|
|
1478
|
-
callback_server_thread.start()
|
|
1479
|
-
|
|
1480
|
-
# Wait for server to be ready (up to 5 seconds)
|
|
1481
|
-
if not server_ready.wait(timeout=5):
|
|
1482
|
-
print("Warning: Timeout waiting for server to start, but continuing anyway")
|
|
1483
|
-
|
|
1484
|
-
callback_server_running = True
|
|
1485
|
-
|
|
1486
|
-
# Verify the server is actually accepting connections
|
|
1487
|
-
try:
|
|
1488
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1489
|
-
s.settimeout(2)
|
|
1490
|
-
s.connect(('localhost', port))
|
|
1491
|
-
print(f"Confirmed server is accepting connections on port {port}")
|
|
1492
|
-
except Exception as e:
|
|
1493
|
-
print(f"Warning: Could not verify server connection: {e}")
|
|
1494
|
-
|
|
1495
|
-
return port
|
|
1496
|
-
|
|
1497
|
-
except Exception as e:
|
|
1498
|
-
print(f"Error starting callback server: {e}")
|
|
1499
|
-
# Try again with a different port in case of bind issues
|
|
1500
|
-
if "address already in use" in str(e).lower():
|
|
1501
|
-
print("Port may be in use, trying a different port...")
|
|
1502
|
-
return start_callback_server() # Recursive call with a new port
|
|
1503
|
-
raise e
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
259
|
def process_token_response(token_container):
|
|
1507
260
|
"""Process the token response from Facebook."""
|
|
1508
261
|
global needs_authentication, auth_manager
|