meta-ads-mcp-python 1.0.79__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meta_ads_mcp/__init__.py +79 -0
- meta_ads_mcp/__main__.py +10 -0
- meta_ads_mcp/core/__init__.py +55 -0
- meta_ads_mcp/core/accounts.py +141 -0
- meta_ads_mcp/core/ads.py +2751 -0
- meta_ads_mcp/core/ads_library.py +74 -0
- meta_ads_mcp/core/adsets.py +666 -0
- meta_ads_mcp/core/api.py +431 -0
- meta_ads_mcp/core/auth.py +567 -0
- meta_ads_mcp/core/authentication.py +207 -0
- meta_ads_mcp/core/budget_schedules.py +70 -0
- meta_ads_mcp/core/callback_server.py +256 -0
- meta_ads_mcp/core/campaigns.py +379 -0
- meta_ads_mcp/core/duplication.py +523 -0
- meta_ads_mcp/core/http_auth_integration.py +307 -0
- meta_ads_mcp/core/insights.py +161 -0
- meta_ads_mcp/core/mcc.py +232 -0
- meta_ads_mcp/core/openai_deep_research.py +418 -0
- meta_ads_mcp/core/pipeboard_auth.py +510 -0
- meta_ads_mcp/core/reports.py +135 -0
- meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp/core/server.py +391 -0
- meta_ads_mcp/core/targeting.py +542 -0
- meta_ads_mcp/core/utils.py +225 -0
- meta_ads_mcp/settings.py +33 -0
- meta_ads_mcp_python-1.0.79.dist-info/METADATA +187 -0
- meta_ads_mcp_python-1.0.79.dist-info/RECORD +29 -0
- meta_ads_mcp_python-1.0.79.dist-info/WHEEL +4 -0
- meta_ads_mcp_python-1.0.79.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Authentication-specific functionality for Meta Ads API.
|
|
2
|
+
|
|
3
|
+
The Meta Ads MCP server supports three authentication modes:
|
|
4
|
+
|
|
5
|
+
1. **Development/Local Mode** (default)
|
|
6
|
+
- Uses local callback server on localhost:8080+ for OAuth redirect
|
|
7
|
+
- Requires META_ADS_DISABLE_CALLBACK_SERVER to NOT be set
|
|
8
|
+
- Best for local development and testing
|
|
9
|
+
|
|
10
|
+
2. **Production with API Token**
|
|
11
|
+
- Uses PIPEBOARD_API_TOKEN for server-to-server authentication
|
|
12
|
+
- Bypasses OAuth flow entirely
|
|
13
|
+
- Best for server deployments with pre-configured tokens
|
|
14
|
+
|
|
15
|
+
3. **Production OAuth Flow** (NEW)
|
|
16
|
+
- Uses Pipeboard OAuth endpoints for dynamic client registration
|
|
17
|
+
- Triggered when META_ADS_DISABLE_CALLBACK_SERVER is set but no PIPEBOARD_API_TOKEN
|
|
18
|
+
- Supports MCP clients that implement OAuth 2.0 discovery
|
|
19
|
+
|
|
20
|
+
Environment Variables:
|
|
21
|
+
- PIPEBOARD_API_TOKEN: Enables mode 2 (token-based auth)
|
|
22
|
+
- META_ADS_DISABLE_CALLBACK_SERVER: Disables local server, enables mode 3
|
|
23
|
+
- META_ACCESS_TOKEN: Direct Meta token (fallback)
|
|
24
|
+
- META_ADS_DISABLE_LOGIN_LINK: Hard-disables the get_login_link tool; returns a disabled message
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from typing import Optional
|
|
29
|
+
import asyncio
|
|
30
|
+
import os
|
|
31
|
+
from .api import meta_api_tool
|
|
32
|
+
from . import auth
|
|
33
|
+
from .auth import start_callback_server, shutdown_callback_server, auth_manager
|
|
34
|
+
from .server import mcp_server
|
|
35
|
+
from .utils import logger, META_APP_SECRET
|
|
36
|
+
from .pipeboard_auth import pipeboard_auth_manager
|
|
37
|
+
|
|
38
|
+
# Only register the login link tool if not explicitly disabled
|
|
39
|
+
ENABLE_LOGIN_LINK = not bool(os.environ.get("META_ADS_DISABLE_LOGIN_LINK", ""))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def get_login_link(access_token: Optional[str] = None) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Get a clickable login link for Meta Ads authentication.
|
|
45
|
+
|
|
46
|
+
NOTE: This method should only be used if you're using your own Facebook app.
|
|
47
|
+
If using Pipeboard authentication (recommended), set the PIPEBOARD_API_TOKEN
|
|
48
|
+
environment variable instead (token obtainable via https://pipeboard.co).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A clickable resource link for Meta authentication
|
|
55
|
+
"""
|
|
56
|
+
# Check if we're using pipeboard authentication
|
|
57
|
+
using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
|
|
58
|
+
callback_server_disabled = bool(os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER", ""))
|
|
59
|
+
|
|
60
|
+
if using_pipeboard:
|
|
61
|
+
# Pipeboard token-based authentication
|
|
62
|
+
try:
|
|
63
|
+
logger.info("Using Pipeboard token-based authentication")
|
|
64
|
+
|
|
65
|
+
# If an access token was provided, this is likely a test - return success
|
|
66
|
+
if access_token:
|
|
67
|
+
return json.dumps({
|
|
68
|
+
"message": " Authentication Token Provided",
|
|
69
|
+
"status": "Using provided access token for authentication",
|
|
70
|
+
"token_info": f"Token preview: {access_token[:10]}...",
|
|
71
|
+
"authentication_method": "manual_token",
|
|
72
|
+
"ready_to_use": "You can now use all Meta Ads MCP tools and commands."
|
|
73
|
+
}, indent=2)
|
|
74
|
+
|
|
75
|
+
# Check if Pipeboard token is working
|
|
76
|
+
token = pipeboard_auth_manager.get_access_token()
|
|
77
|
+
if token:
|
|
78
|
+
return json.dumps({
|
|
79
|
+
"message": " Already Authenticated",
|
|
80
|
+
"status": "You're successfully authenticated with Meta Ads via Pipeboard!",
|
|
81
|
+
"token_info": f"Token preview: {token[:10]}...",
|
|
82
|
+
"authentication_method": "pipeboard_token",
|
|
83
|
+
"ready_to_use": "You can now use all Meta Ads MCP tools and commands."
|
|
84
|
+
}, indent=2)
|
|
85
|
+
|
|
86
|
+
# Start Pipeboard auth flow
|
|
87
|
+
auth_data = pipeboard_auth_manager.initiate_auth_flow()
|
|
88
|
+
login_url = auth_data.get('loginUrl')
|
|
89
|
+
|
|
90
|
+
if login_url:
|
|
91
|
+
return json.dumps({
|
|
92
|
+
"message": " Click to Authenticate",
|
|
93
|
+
"login_url": login_url,
|
|
94
|
+
"markdown_link": f"[ Authenticate with Meta Ads]({login_url})",
|
|
95
|
+
"instructions": "Click the link above to complete authentication with Meta Ads.",
|
|
96
|
+
"authentication_method": "pipeboard_oauth",
|
|
97
|
+
"what_happens_next": "After clicking, you'll be redirected to Meta's authentication page. Once completed, your token will be automatically saved.",
|
|
98
|
+
"token_duration": "Your token will be valid for approximately 60 days."
|
|
99
|
+
}, indent=2)
|
|
100
|
+
else:
|
|
101
|
+
return json.dumps({
|
|
102
|
+
"message": " Authentication Error",
|
|
103
|
+
"error": "Could not generate authentication URL from Pipeboard",
|
|
104
|
+
"troubleshooting": [
|
|
105
|
+
"Check that your PIPEBOARD_API_TOKEN is valid",
|
|
106
|
+
"Ensure the Pipeboard service is accessible",
|
|
107
|
+
"Try again in a few moments"
|
|
108
|
+
],
|
|
109
|
+
"authentication_method": "pipeboard_oauth_failed"
|
|
110
|
+
}, indent=2)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error initiating Pipeboard auth flow: {e}")
|
|
114
|
+
return json.dumps({
|
|
115
|
+
"message": " Pipeboard Authentication Error",
|
|
116
|
+
"error": f"Failed to initiate Pipeboard authentication: {str(e)}",
|
|
117
|
+
"troubleshooting": [
|
|
118
|
+
" Check that PIPEBOARD_API_TOKEN environment variable is set correctly",
|
|
119
|
+
" Verify that pipeboard.co is accessible from your network",
|
|
120
|
+
" Try refreshing your Pipeboard API token",
|
|
121
|
+
" Wait a moment and try again"
|
|
122
|
+
],
|
|
123
|
+
"get_help": "Contact support if the issue persists",
|
|
124
|
+
"authentication_method": "pipeboard_error"
|
|
125
|
+
}, indent=2)
|
|
126
|
+
elif callback_server_disabled:
|
|
127
|
+
# Production OAuth flow - use Pipeboard OAuth endpoints directly
|
|
128
|
+
logger.info("Production OAuth flow - using Pipeboard OAuth endpoints")
|
|
129
|
+
|
|
130
|
+
return json.dumps({
|
|
131
|
+
"message": " Authentication Required",
|
|
132
|
+
"instructions": "Please sign in to your Pipeboard account to authenticate with Meta Ads.",
|
|
133
|
+
"sign_in_url": "https://pipeboard.co/auth/signin",
|
|
134
|
+
"markdown_link": "[ Sign in to Pipeboard](https://pipeboard.co/auth/signin)",
|
|
135
|
+
"what_to_do": "Click the link above to sign in to your Pipeboard account and complete authentication.",
|
|
136
|
+
"authentication_method": "production_oauth"
|
|
137
|
+
}, indent=2)
|
|
138
|
+
else:
|
|
139
|
+
# Original Meta authentication flow (development/local)
|
|
140
|
+
# Check if we have a cached token
|
|
141
|
+
cached_token = auth_manager.get_access_token()
|
|
142
|
+
token_status = "No token" if not cached_token else "Valid token"
|
|
143
|
+
|
|
144
|
+
# If we already have a valid token and none was provided, just return success
|
|
145
|
+
if cached_token and not access_token:
|
|
146
|
+
logger.info("get_login_link called with existing valid token")
|
|
147
|
+
return json.dumps({
|
|
148
|
+
"message": " Already Authenticated",
|
|
149
|
+
"status": "You're successfully authenticated with Meta Ads!",
|
|
150
|
+
"token_info": f"Token preview: {cached_token[:10]}...",
|
|
151
|
+
"created_at": auth_manager.token_info.created_at if hasattr(auth_manager, "token_info") else None,
|
|
152
|
+
"expires_in": auth_manager.token_info.expires_in if hasattr(auth_manager, "token_info") else None,
|
|
153
|
+
"authentication_method": "meta_oauth",
|
|
154
|
+
"ready_to_use": "You can now use all Meta Ads MCP tools and commands."
|
|
155
|
+
}, indent=2)
|
|
156
|
+
|
|
157
|
+
# IMPORTANT: Start the callback server first by calling our helper function
|
|
158
|
+
# This ensures the server is ready before we provide the URL to the user
|
|
159
|
+
logger.info("Starting callback server for authentication")
|
|
160
|
+
try:
|
|
161
|
+
port = start_callback_server()
|
|
162
|
+
logger.info(f"Callback server started on port {port}")
|
|
163
|
+
|
|
164
|
+
# Generate direct login URL
|
|
165
|
+
auth_manager.redirect_uri = f"http://localhost:{port}/callback" # Ensure port is set correctly
|
|
166
|
+
logger.info(f"Setting redirect URI to {auth_manager.redirect_uri}")
|
|
167
|
+
login_url = auth_manager.get_auth_url()
|
|
168
|
+
logger.info(f"Generated login URL: {login_url}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to start callback server: {e}")
|
|
171
|
+
return json.dumps({
|
|
172
|
+
"message": " Local Authentication Unavailable",
|
|
173
|
+
"error": "Cannot start local callback server for authentication",
|
|
174
|
+
"reason": str(e),
|
|
175
|
+
"solutions": [
|
|
176
|
+
" Use Pipeboard authentication: Set PIPEBOARD_API_TOKEN environment variable",
|
|
177
|
+
" Use direct token: Set META_ACCESS_TOKEN environment variable",
|
|
178
|
+
" Check if another service is using the required ports"
|
|
179
|
+
],
|
|
180
|
+
"authentication_method": "meta_oauth_disabled"
|
|
181
|
+
}, indent=2)
|
|
182
|
+
|
|
183
|
+
# Check if we can exchange for long-lived tokens
|
|
184
|
+
token_exchange_supported = bool(META_APP_SECRET)
|
|
185
|
+
token_duration = "60 days" if token_exchange_supported else "1-2 hours"
|
|
186
|
+
|
|
187
|
+
# Return a special format that helps the LLM format the response properly
|
|
188
|
+
response = {
|
|
189
|
+
"message": " Click to Authenticate",
|
|
190
|
+
"login_url": login_url,
|
|
191
|
+
"markdown_link": f"[ Authenticate with Meta Ads]({login_url})",
|
|
192
|
+
"instructions": "Click the link above to authenticate with Meta Ads.",
|
|
193
|
+
"server_info": f"Local callback server running on port {port}",
|
|
194
|
+
"token_duration": f"Your token will be valid for approximately {token_duration}",
|
|
195
|
+
"authentication_method": "meta_oauth",
|
|
196
|
+
"what_happens_next": "After clicking, you'll be redirected to Meta's authentication page. Once completed, your token will be automatically saved.",
|
|
197
|
+
"security_note": "This uses a secure local callback server for development purposes."
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Wait a moment to ensure the server is fully started
|
|
201
|
+
await asyncio.sleep(1)
|
|
202
|
+
|
|
203
|
+
return json.dumps(response, indent=2)
|
|
204
|
+
|
|
205
|
+
# Conditionally register as MCP tool only when enabled
|
|
206
|
+
if ENABLE_LOGIN_LINK:
|
|
207
|
+
get_login_link = mcp_server.tool()(get_login_link)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Budget Schedule-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
from .api import meta_api_tool, make_api_request
|
|
7
|
+
from .server import mcp_server
|
|
8
|
+
# Assuming no other specific dependencies from adsets.py are needed for this single function.
|
|
9
|
+
# If other utilities from adsets.py (like get_ad_accounts) were needed, they'd be imported here.
|
|
10
|
+
|
|
11
|
+
@meta_api_tool
|
|
12
|
+
async def create_budget_schedule(
|
|
13
|
+
campaign_id: str,
|
|
14
|
+
budget_value: int,
|
|
15
|
+
budget_value_type: str,
|
|
16
|
+
time_start: int,
|
|
17
|
+
time_end: int,
|
|
18
|
+
access_token: Optional[str] = None
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Create a budget schedule for a Meta Ads campaign.
|
|
22
|
+
|
|
23
|
+
Allows scheduling budget increases based on anticipated high-demand periods.
|
|
24
|
+
The times should be provided as Unix timestamps.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
campaign_id: Meta Ads campaign ID.
|
|
28
|
+
budget_value: Amount of budget increase. Interpreted based on budget_value_type.
|
|
29
|
+
budget_value_type: Type of budget value - "ABSOLUTE" or "MULTIPLIER".
|
|
30
|
+
time_start: Unix timestamp for when the high demand period should start.
|
|
31
|
+
time_end: Unix timestamp for when the high demand period should end.
|
|
32
|
+
access_token: Meta API access token (optional - will use cached token if not provided).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A JSON string containing the ID of the created budget schedule or an error message.
|
|
36
|
+
"""
|
|
37
|
+
if not campaign_id:
|
|
38
|
+
return json.dumps({"error": "Campaign ID is required"}, indent=2)
|
|
39
|
+
if budget_value is None: # Check for None explicitly
|
|
40
|
+
return json.dumps({"error": "Budget value is required"}, indent=2)
|
|
41
|
+
if not budget_value_type:
|
|
42
|
+
return json.dumps({"error": "Budget value type is required"}, indent=2)
|
|
43
|
+
if budget_value_type not in ["ABSOLUTE", "MULTIPLIER"]:
|
|
44
|
+
return json.dumps({"error": "Invalid budget_value_type. Must be ABSOLUTE or MULTIPLIER"}, indent=2)
|
|
45
|
+
if time_start is None: # Check for None explicitly to allow 0
|
|
46
|
+
return json.dumps({"error": "Time start is required"}, indent=2)
|
|
47
|
+
if time_end is None: # Check for None explicitly to allow 0
|
|
48
|
+
return json.dumps({"error": "Time end is required"}, indent=2)
|
|
49
|
+
|
|
50
|
+
endpoint = f"{campaign_id}/budget_schedules"
|
|
51
|
+
|
|
52
|
+
params = {
|
|
53
|
+
"budget_value": budget_value,
|
|
54
|
+
"budget_value_type": budget_value_type,
|
|
55
|
+
"time_start": time_start,
|
|
56
|
+
"time_end": time_end,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
61
|
+
return json.dumps(data, indent=2)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
error_msg = str(e)
|
|
64
|
+
# Include details about the error and the parameters sent for easier debugging
|
|
65
|
+
return json.dumps({
|
|
66
|
+
"error": "Failed to create budget schedule",
|
|
67
|
+
"details": error_msg,
|
|
68
|
+
"campaign_id": campaign_id,
|
|
69
|
+
"params_sent": params
|
|
70
|
+
}, indent=2)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Callback server for Meta Ads API authentication."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import socket
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import webbrowser
|
|
8
|
+
import os
|
|
9
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
10
|
+
from urllib.parse import urlparse, parse_qs, quote
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
|
|
13
|
+
from .utils import logger
|
|
14
|
+
|
|
15
|
+
# Global token container for communication between threads
|
|
16
|
+
token_container = {"token": None, "expires_in": None, "user_id": None}
|
|
17
|
+
|
|
18
|
+
# Global variables for server thread and state
|
|
19
|
+
callback_server_thread = None
|
|
20
|
+
callback_server_lock = threading.Lock()
|
|
21
|
+
callback_server_running = False
|
|
22
|
+
callback_server_port = None
|
|
23
|
+
callback_server_instance = None
|
|
24
|
+
server_shutdown_timer = None
|
|
25
|
+
|
|
26
|
+
# Timeout in seconds before shutting down the callback server
|
|
27
|
+
CALLBACK_SERVER_TIMEOUT = 180 # 3 minutes timeout
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
31
|
+
def do_GET(self):
|
|
32
|
+
try:
|
|
33
|
+
# Print path for debugging
|
|
34
|
+
print(f"Callback server received request: {self.path}")
|
|
35
|
+
|
|
36
|
+
if self.path.startswith("/callback"):
|
|
37
|
+
self._handle_oauth_callback()
|
|
38
|
+
elif self.path.startswith("/token"):
|
|
39
|
+
self._handle_token()
|
|
40
|
+
else:
|
|
41
|
+
# If no matching path, return a 404 error
|
|
42
|
+
self.send_response(404)
|
|
43
|
+
self.end_headers()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"Error processing request: {e}")
|
|
46
|
+
self.send_response(500)
|
|
47
|
+
self.end_headers()
|
|
48
|
+
|
|
49
|
+
def _handle_oauth_callback(self):
|
|
50
|
+
"""Handle OAuth callback after user authorization"""
|
|
51
|
+
# Check if we're being redirected from Facebook with an authorization code
|
|
52
|
+
parsed_url = urlparse(self.path)
|
|
53
|
+
params = parse_qs(parsed_url.query)
|
|
54
|
+
|
|
55
|
+
# Check for code parameter
|
|
56
|
+
code = params.get('code', [None])[0]
|
|
57
|
+
state = params.get('state', [None])[0]
|
|
58
|
+
error = params.get('error', [None])[0]
|
|
59
|
+
|
|
60
|
+
# Send 200 OK response with a simple HTML page
|
|
61
|
+
self.send_response(200)
|
|
62
|
+
self.send_header("Content-type", "text/html")
|
|
63
|
+
self.end_headers()
|
|
64
|
+
|
|
65
|
+
if error:
|
|
66
|
+
# User denied access or other error occurred
|
|
67
|
+
html = f"""
|
|
68
|
+
<html>
|
|
69
|
+
<head><title>Authorization Failed</title></head>
|
|
70
|
+
<body>
|
|
71
|
+
<h1>Authorization Failed</h1>
|
|
72
|
+
<p>Error: {error}</p>
|
|
73
|
+
<p>The authorization was cancelled or failed. You can close this window.</p>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
76
|
+
"""
|
|
77
|
+
logger.error(f"OAuth authorization failed: {error}")
|
|
78
|
+
elif code:
|
|
79
|
+
# Success case - we have the authorization code
|
|
80
|
+
logger.info(f"Received authorization code: {code[:10]}...")
|
|
81
|
+
|
|
82
|
+
# Store the authorization code temporarily
|
|
83
|
+
# The auth module will exchange this for an access token
|
|
84
|
+
token_container.update({
|
|
85
|
+
"auth_code": code,
|
|
86
|
+
"state": state,
|
|
87
|
+
"timestamp": asyncio.get_event_loop().time()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
html = """
|
|
91
|
+
<html>
|
|
92
|
+
<head><title>Authorization Successful</title></head>
|
|
93
|
+
<body>
|
|
94
|
+
<h1> Authorization Successful!</h1>
|
|
95
|
+
<p>You have successfully authorized the Meta Ads MCP application.</p>
|
|
96
|
+
<p>You can now close this window and return to your application.</p>
|
|
97
|
+
<script>
|
|
98
|
+
// Try to close the window automatically after 2 seconds
|
|
99
|
+
setTimeout(function() {
|
|
100
|
+
window.close();
|
|
101
|
+
}, 2000);
|
|
102
|
+
</script>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
"""
|
|
106
|
+
logger.info("OAuth authorization successful")
|
|
107
|
+
else:
|
|
108
|
+
# No code or error - something unexpected happened
|
|
109
|
+
html = """
|
|
110
|
+
<html>
|
|
111
|
+
<head><title>Unexpected Response</title></head>
|
|
112
|
+
<body>
|
|
113
|
+
<h1>Unexpected Response</h1>
|
|
114
|
+
<p>No authorization code or error received. Please try again.</p>
|
|
115
|
+
</body>
|
|
116
|
+
</html>
|
|
117
|
+
"""
|
|
118
|
+
logger.warning("OAuth callback received without code or error")
|
|
119
|
+
|
|
120
|
+
self.wfile.write(html.encode())
|
|
121
|
+
|
|
122
|
+
def _handle_token(self):
|
|
123
|
+
"""Handle token endpoint for retrieving stored token data"""
|
|
124
|
+
# This endpoint allows other parts of the application to retrieve
|
|
125
|
+
# token information from the callback server
|
|
126
|
+
|
|
127
|
+
self.send_response(200)
|
|
128
|
+
self.send_header("Content-type", "application/json")
|
|
129
|
+
self.end_headers()
|
|
130
|
+
|
|
131
|
+
# Return current token container contents
|
|
132
|
+
response_data = {
|
|
133
|
+
"status": "success",
|
|
134
|
+
"data": token_container
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
self.wfile.write(json.dumps(response_data).encode())
|
|
138
|
+
|
|
139
|
+
# The actual token processing is now handled by the auth module
|
|
140
|
+
# that imports this module and accesses token_container
|
|
141
|
+
|
|
142
|
+
# Silence server logs
|
|
143
|
+
def log_message(self, format, *args):
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def shutdown_callback_server():
|
|
148
|
+
"""
|
|
149
|
+
Shutdown the callback server if it's running
|
|
150
|
+
"""
|
|
151
|
+
global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
|
|
152
|
+
|
|
153
|
+
with callback_server_lock:
|
|
154
|
+
if not callback_server_running:
|
|
155
|
+
print("Callback server is not running")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if server_shutdown_timer is not None:
|
|
159
|
+
server_shutdown_timer.cancel()
|
|
160
|
+
server_shutdown_timer = None
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
if callback_server_instance:
|
|
164
|
+
print("Shutting down callback server...")
|
|
165
|
+
callback_server_instance.shutdown()
|
|
166
|
+
callback_server_instance.server_close()
|
|
167
|
+
print("Callback server shut down successfully")
|
|
168
|
+
|
|
169
|
+
if callback_server_thread and callback_server_thread.is_alive():
|
|
170
|
+
callback_server_thread.join(timeout=5)
|
|
171
|
+
if callback_server_thread.is_alive():
|
|
172
|
+
print("Warning: Callback server thread did not shut down cleanly")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"Error during callback server shutdown: {e}")
|
|
175
|
+
finally:
|
|
176
|
+
callback_server_running = False
|
|
177
|
+
callback_server_thread = None
|
|
178
|
+
callback_server_port = None
|
|
179
|
+
callback_server_instance = None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def start_callback_server() -> int:
|
|
183
|
+
"""
|
|
184
|
+
Start the callback server and return the port number it's running on.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
int: Port number the server is listening on
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
Exception: If the server fails to start
|
|
191
|
+
"""
|
|
192
|
+
global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
|
|
193
|
+
|
|
194
|
+
# Check if callback server is disabled
|
|
195
|
+
if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
|
|
196
|
+
raise Exception("Callback server is disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
|
|
197
|
+
|
|
198
|
+
with callback_server_lock:
|
|
199
|
+
if callback_server_running:
|
|
200
|
+
print(f"Callback server already running on port {callback_server_port}")
|
|
201
|
+
return callback_server_port
|
|
202
|
+
|
|
203
|
+
# Find an available port
|
|
204
|
+
port = 8080
|
|
205
|
+
max_attempts = 10
|
|
206
|
+
for attempt in range(max_attempts):
|
|
207
|
+
try:
|
|
208
|
+
# Test if port is available
|
|
209
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
210
|
+
s.bind(('localhost', port))
|
|
211
|
+
break
|
|
212
|
+
except OSError:
|
|
213
|
+
port += 1
|
|
214
|
+
else:
|
|
215
|
+
raise Exception(f"Could not find an available port after {max_attempts} attempts")
|
|
216
|
+
|
|
217
|
+
callback_server_port = port
|
|
218
|
+
|
|
219
|
+
# Start the server in a separate thread
|
|
220
|
+
callback_server_thread = threading.Thread(target=server_thread, daemon=True)
|
|
221
|
+
callback_server_thread.start()
|
|
222
|
+
|
|
223
|
+
# Wait a moment for the server to start
|
|
224
|
+
import time
|
|
225
|
+
time.sleep(0.5)
|
|
226
|
+
|
|
227
|
+
if not callback_server_running:
|
|
228
|
+
raise Exception("Failed to start callback server")
|
|
229
|
+
|
|
230
|
+
# Set up automatic shutdown timer
|
|
231
|
+
def auto_shutdown():
|
|
232
|
+
print(f"Callback server auto-shutdown after {CALLBACK_SERVER_TIMEOUT} seconds")
|
|
233
|
+
shutdown_callback_server()
|
|
234
|
+
|
|
235
|
+
server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, auto_shutdown)
|
|
236
|
+
server_shutdown_timer.start()
|
|
237
|
+
|
|
238
|
+
print(f"Callback server started on http://localhost:{port}")
|
|
239
|
+
return port
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def server_thread():
|
|
243
|
+
"""Thread function to run the callback server"""
|
|
244
|
+
global callback_server_running, callback_server_instance
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
callback_server_instance = HTTPServer(('localhost', callback_server_port), CallbackHandler)
|
|
248
|
+
callback_server_running = True
|
|
249
|
+
print(f"Callback server thread started on port {callback_server_port}")
|
|
250
|
+
callback_server_instance.serve_forever()
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print(f"Callback server error: {e}")
|
|
253
|
+
callback_server_running = False
|
|
254
|
+
finally:
|
|
255
|
+
print("Callback server thread finished")
|
|
256
|
+
callback_server_running = False
|