meta-ads-mcp 0.2.5__py3-none-any.whl → 0.2.8__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/api.py +122 -53
- meta_ads_mcp/core/__init__.py +2 -1
- meta_ads_mcp/core/ads.py +61 -1
- meta_ads_mcp/core/adsets.py +8 -3
- meta_ads_mcp/core/api.py +29 -1
- meta_ads_mcp/core/auth.py +91 -1270
- meta_ads_mcp/core/authentication.py +103 -49
- meta_ads_mcp/core/callback_server.py +958 -0
- meta_ads_mcp/core/pipeboard_auth.py +484 -0
- meta_ads_mcp/core/server.py +49 -4
- meta_ads_mcp/core/utils.py +11 -5
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.8.dist-info}/METADATA +139 -32
- meta_ads_mcp-0.2.8.dist-info/RECORD +21 -0
- meta_ads_mcp-0.2.5.dist-info/RECORD +0 -19
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.8.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.2.5.dist-info → meta_ads_mcp-0.2.8.dist-info}/entry_points.txt +0 -0
meta_ads_mcp/core/auth.py
CHANGED
|
@@ -5,18 +5,24 @@ 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
|
+
|
|
21
|
+
# Import the new Pipeboard authentication
|
|
22
|
+
from .pipeboard_auth import pipeboard_auth_manager
|
|
23
|
+
|
|
18
24
|
# Auth constants
|
|
19
|
-
AUTH_SCOPE = "ads_management,ads_read,business_management"
|
|
25
|
+
AUTH_SCOPE = "ads_management,ads_read,business_management,public_profile"
|
|
20
26
|
AUTH_REDIRECT_URI = "http://localhost:8888/callback"
|
|
21
27
|
AUTH_RESPONSE_TYPE = "token"
|
|
22
28
|
|
|
@@ -25,21 +31,9 @@ logger.info("Authentication module initialized")
|
|
|
25
31
|
logger.info(f"Auth scope: {AUTH_SCOPE}")
|
|
26
32
|
logger.info(f"Default redirect URI: {AUTH_REDIRECT_URI}")
|
|
27
33
|
|
|
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
34
|
# Global flag for authentication state
|
|
35
35
|
needs_authentication = False
|
|
36
36
|
|
|
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
37
|
# Meta configuration singleton
|
|
44
38
|
class MetaConfig:
|
|
45
39
|
_instance = None
|
|
@@ -132,7 +126,10 @@ class AuthManager:
|
|
|
132
126
|
self.app_id = app_id
|
|
133
127
|
self.redirect_uri = redirect_uri
|
|
134
128
|
self.token_info = None
|
|
135
|
-
|
|
129
|
+
# Check for Pipeboard token first
|
|
130
|
+
self.use_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
|
|
131
|
+
if not self.use_pipeboard:
|
|
132
|
+
self._load_cached_token()
|
|
136
133
|
|
|
137
134
|
def _get_token_cache_path(self) -> pathlib.Path:
|
|
138
135
|
"""Get the platform-specific path for token cache file"""
|
|
@@ -163,14 +160,14 @@ class AuthManager:
|
|
|
163
160
|
|
|
164
161
|
# Check if token is expired
|
|
165
162
|
if self.token_info.is_expired():
|
|
166
|
-
|
|
163
|
+
logger.info("Cached token is expired")
|
|
167
164
|
self.token_info = None
|
|
168
165
|
return False
|
|
169
166
|
|
|
170
|
-
|
|
167
|
+
logger.info(f"Loaded cached token (expires in {(self.token_info.created_at + self.token_info.expires_in) - int(time.time())} seconds)")
|
|
171
168
|
return True
|
|
172
169
|
except Exception as e:
|
|
173
|
-
|
|
170
|
+
logger.error(f"Error loading cached token: {e}")
|
|
174
171
|
return False
|
|
175
172
|
|
|
176
173
|
def _save_token_to_cache(self) -> None:
|
|
@@ -183,9 +180,9 @@ class AuthManager:
|
|
|
183
180
|
try:
|
|
184
181
|
with open(cache_path, "w") as f:
|
|
185
182
|
json.dump(self.token_info.serialize(), f)
|
|
186
|
-
|
|
183
|
+
logger.info(f"Token cached at: {cache_path}")
|
|
187
184
|
except Exception as e:
|
|
188
|
-
|
|
185
|
+
logger.error(f"Error saving token to cache: {e}")
|
|
189
186
|
|
|
190
187
|
def get_auth_url(self) -> str:
|
|
191
188
|
"""Generate the Facebook OAuth URL for desktop app flow"""
|
|
@@ -207,6 +204,12 @@ class AuthManager:
|
|
|
207
204
|
Returns:
|
|
208
205
|
Access token if successful, None otherwise
|
|
209
206
|
"""
|
|
207
|
+
# If Pipeboard auth is available, use that instead
|
|
208
|
+
if self.use_pipeboard:
|
|
209
|
+
logger.info("Using Pipeboard authentication")
|
|
210
|
+
return pipeboard_auth_manager.get_access_token(force_refresh=force_refresh)
|
|
211
|
+
|
|
212
|
+
# Otherwise, use the original OAuth flow
|
|
210
213
|
# Check if we already have a valid token
|
|
211
214
|
if not force_refresh and self.token_info and not self.token_info.is_expired():
|
|
212
215
|
return self.token_info.access_token
|
|
@@ -214,11 +217,14 @@ class AuthManager:
|
|
|
214
217
|
# Start the callback server if not already running
|
|
215
218
|
port = start_callback_server()
|
|
216
219
|
|
|
220
|
+
# Update redirect URI with the actual port
|
|
221
|
+
self.redirect_uri = f"http://localhost:{port}/callback"
|
|
222
|
+
|
|
217
223
|
# Generate the auth URL
|
|
218
224
|
auth_url = self.get_auth_url()
|
|
219
225
|
|
|
220
226
|
# Open browser with auth URL
|
|
221
|
-
|
|
227
|
+
logger.info(f"Opening browser with URL: {auth_url}")
|
|
222
228
|
webbrowser.open(auth_url)
|
|
223
229
|
|
|
224
230
|
# We don't wait for the token here anymore
|
|
@@ -233,6 +239,10 @@ class AuthManager:
|
|
|
233
239
|
Returns:
|
|
234
240
|
Access token if available, None otherwise
|
|
235
241
|
"""
|
|
242
|
+
# If using Pipeboard, always delegate to the Pipeboard auth manager
|
|
243
|
+
if self.use_pipeboard:
|
|
244
|
+
return pipeboard_auth_manager.get_access_token()
|
|
245
|
+
|
|
236
246
|
if not self.token_info or self.token_info.is_expired():
|
|
237
247
|
return None
|
|
238
248
|
|
|
@@ -240,8 +250,13 @@ class AuthManager:
|
|
|
240
250
|
|
|
241
251
|
def invalidate_token(self) -> None:
|
|
242
252
|
"""Invalidate the current token, usually because it has expired or is invalid"""
|
|
253
|
+
# If using Pipeboard, delegate to the Pipeboard auth manager
|
|
254
|
+
if self.use_pipeboard:
|
|
255
|
+
pipeboard_auth_manager.invalidate_token()
|
|
256
|
+
return
|
|
257
|
+
|
|
243
258
|
if self.token_info:
|
|
244
|
-
|
|
259
|
+
logger.info(f"Invalidating token: {self.token_info.access_token[:10]}...")
|
|
245
260
|
self.token_info = None
|
|
246
261
|
|
|
247
262
|
# Signal that authentication is needed
|
|
@@ -253,1256 +268,15 @@ class AuthManager:
|
|
|
253
268
|
cache_path = self._get_token_cache_path()
|
|
254
269
|
if cache_path.exists():
|
|
255
270
|
os.remove(cache_path)
|
|
256
|
-
|
|
271
|
+
logger.info(f"Removed cached token file: {cache_path}")
|
|
257
272
|
except Exception as e:
|
|
258
|
-
|
|
273
|
+
logger.error(f"Error removing cached token file: {e}")
|
|
259
274
|
|
|
260
275
|
def clear_token(self) -> None:
|
|
261
|
-
"""
|
|
276
|
+
"""Alias for invalidate_token for consistency with other APIs"""
|
|
262
277
|
self.invalidate_token()
|
|
263
278
|
|
|
264
279
|
|
|
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
280
|
def process_token_response(token_container):
|
|
1507
281
|
"""Process the token response from Facebook."""
|
|
1508
282
|
global needs_authentication, auth_manager
|
|
@@ -1632,18 +406,55 @@ async def get_current_access_token() -> Optional[str]:
|
|
|
1632
406
|
app_id = meta_config.get_app_id()
|
|
1633
407
|
logger.debug(f"Current app_id: {app_id}")
|
|
1634
408
|
|
|
409
|
+
# Check if using Pipeboard authentication
|
|
410
|
+
using_pipeboard = auth_manager.use_pipeboard
|
|
411
|
+
|
|
412
|
+
# Check if app_id is valid - but only if not using Pipeboard authentication
|
|
413
|
+
if not app_id and not using_pipeboard:
|
|
414
|
+
logger.error("TOKEN VALIDATION FAILED: No valid app_id configured")
|
|
415
|
+
logger.error("Please set META_APP_ID environment variable or configure via meta_config.set_app_id()")
|
|
416
|
+
return None
|
|
417
|
+
|
|
1635
418
|
# Attempt to get access token
|
|
1636
419
|
try:
|
|
1637
420
|
token = auth_manager.get_access_token()
|
|
1638
421
|
|
|
1639
422
|
if token:
|
|
1640
|
-
|
|
423
|
+
# Add basic token validation - check if it looks like a valid token
|
|
424
|
+
if len(token) < 20: # Most Meta tokens are much longer
|
|
425
|
+
logger.error(f"TOKEN VALIDATION FAILED: Token appears malformed (length: {len(token)})")
|
|
426
|
+
auth_manager.invalidate_token()
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
logger.debug(f"Access token found in auth_manager (starts with: {token[:10]}...)")
|
|
1641
430
|
return token
|
|
1642
431
|
else:
|
|
1643
432
|
logger.warning("No valid access token available in auth_manager")
|
|
433
|
+
|
|
434
|
+
# Check why token might be missing
|
|
435
|
+
if hasattr(auth_manager, 'token_info') and auth_manager.token_info:
|
|
436
|
+
if auth_manager.token_info.is_expired():
|
|
437
|
+
logger.error("TOKEN VALIDATION FAILED: Token is expired")
|
|
438
|
+
# Add expiration details
|
|
439
|
+
if hasattr(auth_manager.token_info, 'expires_in') and auth_manager.token_info.expires_in:
|
|
440
|
+
expiry_time = auth_manager.token_info.created_at + auth_manager.token_info.expires_in
|
|
441
|
+
current_time = int(time.time())
|
|
442
|
+
expired_seconds_ago = current_time - expiry_time
|
|
443
|
+
logger.error(f"Token expired {expired_seconds_ago} seconds ago")
|
|
444
|
+
elif not auth_manager.token_info.access_token:
|
|
445
|
+
logger.error("TOKEN VALIDATION FAILED: Token object exists but access_token is empty")
|
|
446
|
+
else:
|
|
447
|
+
logger.error("TOKEN VALIDATION FAILED: Token exists but was rejected for unknown reason")
|
|
448
|
+
else:
|
|
449
|
+
logger.error("TOKEN VALIDATION FAILED: No token information available")
|
|
450
|
+
|
|
451
|
+
# Suggest next steps for troubleshooting
|
|
452
|
+
logger.error("To fix: Try re-authenticating or check if your token has been revoked")
|
|
1644
453
|
return None
|
|
1645
454
|
except Exception as e:
|
|
1646
455
|
logger.error(f"Error getting access token: {str(e)}")
|
|
456
|
+
import traceback
|
|
457
|
+
logger.error(f"Token validation stacktrace: {traceback.format_exc()}")
|
|
1647
458
|
return None
|
|
1648
459
|
|
|
1649
460
|
|
|
@@ -1690,4 +501,14 @@ def login():
|
|
|
1690
501
|
|
|
1691
502
|
# Initialize auth manager with a placeholder - will be updated at runtime
|
|
1692
503
|
META_APP_ID = os.environ.get("META_APP_ID", "YOUR_META_APP_ID")
|
|
504
|
+
|
|
505
|
+
# Only show warnings about missing META_APP_ID/META_APP_SECRET when not using Pipeboard
|
|
506
|
+
if not os.environ.get("PIPEBOARD_API_TOKEN"):
|
|
507
|
+
# Log warnings about missing environment variables
|
|
508
|
+
if META_APP_ID == "YOUR_META_APP_ID":
|
|
509
|
+
logger.warning("META_APP_ID environment variable is not set. Authentication will not work properly.")
|
|
510
|
+
|
|
511
|
+
if not os.environ.get("META_APP_SECRET"):
|
|
512
|
+
logger.warning("META_APP_SECRET environment variable is not set. Long-lived token exchange will not work.")
|
|
513
|
+
|
|
1693
514
|
auth_manager = AuthManager(META_APP_ID)
|