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/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
- self._load_cached_token()
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
- print("Cached token is expired")
163
+ logger.info("Cached token is expired")
167
164
  self.token_info = None
168
165
  return False
169
166
 
170
- print(f"Loaded cached token (expires in {(self.token_info.created_at + self.token_info.expires_in) - int(time.time())} seconds)")
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
- print(f"Error loading cached token: {e}")
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
- print(f"Token cached at: {cache_path}")
183
+ logger.info(f"Token cached at: {cache_path}")
187
184
  except Exception as e:
188
- print(f"Error saving token to cache: {e}")
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
- print(f"Opening browser with URL: {auth_url}")
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
- print(f"Invalidating token: {self.token_info.access_token[:10]}...")
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
- print(f"Removed cached token file: {cache_path}")
271
+ logger.info(f"Removed cached token file: {cache_path}")
257
272
  except Exception as e:
258
- print(f"Error removing cached token: {e}")
273
+ logger.error(f"Error removing cached token file: {e}")
259
274
 
260
275
  def clear_token(self) -> None:
261
- """Clear the current token and remove from cache"""
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
- logger.debug("Access token found in auth_manager")
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)