meta-ads-mcp 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl

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