meta-ads-mcp 0.4.3__py3-none-any.whl → 0.4.5__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 +1 -1
- meta_ads_mcp/core/adsets.py +24 -69
- meta_ads_mcp/core/auth.py +3 -1
- meta_ads_mcp/core/callback_server.py +142 -906
- {meta_ads_mcp-0.4.3.dist-info → meta_ads_mcp-0.4.5.dist-info}/METADATA +4 -2
- {meta_ads_mcp-0.4.3.dist-info → meta_ads_mcp-0.4.5.dist-info}/RECORD +9 -9
- {meta_ads_mcp-0.4.3.dist-info → meta_ads_mcp-0.4.5.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.4.3.dist-info → meta_ads_mcp-0.4.5.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.4.3.dist-info → meta_ads_mcp-0.4.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Callback server for Meta Ads API authentication
|
|
1
|
+
"""Callback server for Meta Ads API authentication."""
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
4
|
import socket
|
|
@@ -16,9 +16,6 @@ from .utils import logger
|
|
|
16
16
|
# Global token container for communication between threads
|
|
17
17
|
token_container = {"token": None, "expires_in": None, "user_id": None}
|
|
18
18
|
|
|
19
|
-
# Global container for update confirmations
|
|
20
|
-
update_confirmation = {"approved": False}
|
|
21
|
-
|
|
22
19
|
# Global variables for server thread and state
|
|
23
20
|
callback_server_thread = None
|
|
24
21
|
callback_server_lock = threading.Lock()
|
|
@@ -41,16 +38,6 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
41
38
|
self._handle_oauth_callback()
|
|
42
39
|
elif self.path.startswith("/token"):
|
|
43
40
|
self._handle_token()
|
|
44
|
-
elif self.path.startswith("/confirm-update"):
|
|
45
|
-
self._handle_update_confirmation()
|
|
46
|
-
elif self.path.startswith("/update-confirm"):
|
|
47
|
-
self._handle_update_execution()
|
|
48
|
-
elif self.path.startswith("/verify-update"):
|
|
49
|
-
self._handle_update_verification()
|
|
50
|
-
elif self.path.startswith("/api/adset"):
|
|
51
|
-
self._handle_adset_api()
|
|
52
|
-
elif self.path.startswith("/api/ad"):
|
|
53
|
-
self._handle_ad_api()
|
|
54
41
|
else:
|
|
55
42
|
# If no matching path, return a 404 error
|
|
56
43
|
self.send_response(404)
|
|
@@ -61,821 +48,97 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
61
48
|
self.end_headers()
|
|
62
49
|
|
|
63
50
|
def _handle_oauth_callback(self):
|
|
64
|
-
"""Handle
|
|
65
|
-
|
|
66
|
-
self.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
callback_html = """
|
|
70
|
-
<!DOCTYPE html>
|
|
71
|
-
<html>
|
|
72
|
-
<head>
|
|
73
|
-
<title>Authentication Successful</title>
|
|
74
|
-
<style>
|
|
75
|
-
body {
|
|
76
|
-
font-family: Arial, sans-serif;
|
|
77
|
-
line-height: 1.6;
|
|
78
|
-
color: #333;
|
|
79
|
-
max-width: 800px;
|
|
80
|
-
margin: 0 auto;
|
|
81
|
-
padding: 20px;
|
|
82
|
-
}
|
|
83
|
-
.success {
|
|
84
|
-
color: #4CAF50;
|
|
85
|
-
font-size: 24px;
|
|
86
|
-
margin-bottom: 20px;
|
|
87
|
-
}
|
|
88
|
-
.info {
|
|
89
|
-
background-color: #f5f5f5;
|
|
90
|
-
padding: 15px;
|
|
91
|
-
border-radius: 4px;
|
|
92
|
-
margin-bottom: 20px;
|
|
93
|
-
}
|
|
94
|
-
.button {
|
|
95
|
-
background-color: #4CAF50;
|
|
96
|
-
color: white;
|
|
97
|
-
padding: 10px 15px;
|
|
98
|
-
border: none;
|
|
99
|
-
border-radius: 4px;
|
|
100
|
-
cursor: pointer;
|
|
101
|
-
}
|
|
102
|
-
</style>
|
|
103
|
-
</head>
|
|
104
|
-
<body>
|
|
105
|
-
<div class="success">Authentication Successful!</div>
|
|
106
|
-
<div class="info">
|
|
107
|
-
<p>Your Meta Ads API token has been received.</p>
|
|
108
|
-
<p>You can now close this window and return to the application.</p>
|
|
109
|
-
</div>
|
|
110
|
-
<button class="button" onclick="window.close()">Close Window</button>
|
|
111
|
-
|
|
112
|
-
<script>
|
|
113
|
-
// Function to parse URL parameters including fragments
|
|
114
|
-
function parseURL(url) {
|
|
115
|
-
var params = {};
|
|
116
|
-
var parser = document.createElement('a');
|
|
117
|
-
parser.href = url;
|
|
118
|
-
|
|
119
|
-
// Parse fragment parameters
|
|
120
|
-
var fragment = parser.hash.substring(1);
|
|
121
|
-
var fragmentParams = fragment.split('&');
|
|
122
|
-
|
|
123
|
-
for (var i = 0; i < fragmentParams.length; i++) {
|
|
124
|
-
var pair = fragmentParams[i].split('=');
|
|
125
|
-
params[pair[0]] = decodeURIComponent(pair[1]);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return params;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Parse the URL to get the access token
|
|
132
|
-
var params = parseURL(window.location.href);
|
|
133
|
-
var token = params['access_token'];
|
|
134
|
-
var expires_in = params['expires_in'];
|
|
135
|
-
|
|
136
|
-
// Send the token to the server
|
|
137
|
-
if (token) {
|
|
138
|
-
// Create XMLHttpRequest object
|
|
139
|
-
var xhr = new XMLHttpRequest();
|
|
140
|
-
|
|
141
|
-
// Configure it to make a GET request to the /token endpoint
|
|
142
|
-
xhr.open('GET', '/token?token=' + encodeURIComponent(token) +
|
|
143
|
-
'&expires_in=' + encodeURIComponent(expires_in), true);
|
|
144
|
-
|
|
145
|
-
// Set up a handler for when the request is complete
|
|
146
|
-
xhr.onload = function() {
|
|
147
|
-
if (xhr.status === 200) {
|
|
148
|
-
console.log('Token successfully sent to server');
|
|
149
|
-
} else {
|
|
150
|
-
console.error('Failed to send token to server');
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Send the request
|
|
155
|
-
xhr.send();
|
|
156
|
-
} else {
|
|
157
|
-
console.error('No token found in URL');
|
|
158
|
-
document.body.innerHTML += '<div style="color: red; margin-top: 20px;">Error: No authentication token found. Please try again.</div>';
|
|
159
|
-
}
|
|
160
|
-
</script>
|
|
161
|
-
</body>
|
|
162
|
-
</html>
|
|
163
|
-
"""
|
|
164
|
-
self.wfile.write(callback_html.encode())
|
|
165
|
-
|
|
166
|
-
def _handle_token(self):
|
|
167
|
-
"""Handle the token received from the callback"""
|
|
168
|
-
# Extract token from query params
|
|
169
|
-
query = parse_qs(urlparse(self.path).query)
|
|
170
|
-
token_container["token"] = query.get("token", [""])[0]
|
|
171
|
-
|
|
172
|
-
if "expires_in" in query:
|
|
173
|
-
try:
|
|
174
|
-
token_container["expires_in"] = int(query.get("expires_in", ["0"])[0])
|
|
175
|
-
except ValueError:
|
|
176
|
-
token_container["expires_in"] = None
|
|
177
|
-
|
|
178
|
-
# Send success response
|
|
179
|
-
self.send_response(200)
|
|
180
|
-
self.send_header("Content-type", "text/plain")
|
|
181
|
-
self.end_headers()
|
|
182
|
-
self.wfile.write(b"Token received")
|
|
183
|
-
|
|
184
|
-
# The actual token processing is now handled by the auth module
|
|
185
|
-
# that imports this module and accesses token_container
|
|
186
|
-
|
|
187
|
-
def _handle_update_confirmation(self):
|
|
188
|
-
"""Handle the update confirmation page"""
|
|
189
|
-
# Extract query parameters
|
|
190
|
-
query = parse_qs(urlparse(self.path).query)
|
|
191
|
-
adset_id = query.get("adset_id", [""])[0]
|
|
192
|
-
ad_id = query.get("ad_id", [""])[0]
|
|
193
|
-
token = query.get("token", [""])[0]
|
|
194
|
-
changes = query.get("changes", ["{}"])[0]
|
|
195
|
-
|
|
196
|
-
# Determine what type of object we're updating
|
|
197
|
-
object_id = ad_id if ad_id else adset_id
|
|
198
|
-
object_type = "Ad" if ad_id else "Ad Set"
|
|
51
|
+
"""Handle OAuth callback after user authorization"""
|
|
52
|
+
# Check if we're being redirected from Facebook with an authorization code
|
|
53
|
+
parsed_url = urlparse(self.path)
|
|
54
|
+
params = parse_qs(parsed_url.query)
|
|
199
55
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
56
|
+
# Check for code parameter
|
|
57
|
+
code = params.get('code', [None])[0]
|
|
58
|
+
state = params.get('state', [None])[0]
|
|
59
|
+
error = params.get('error', [None])[0]
|
|
204
60
|
|
|
205
|
-
#
|
|
61
|
+
# Send 200 OK response with a simple HTML page
|
|
206
62
|
self.send_response(200)
|
|
207
|
-
self.send_header("Content-type", "text/html
|
|
208
|
-
self.end_headers()
|
|
209
|
-
|
|
210
|
-
# Create a properly escaped JSON string for JavaScript
|
|
211
|
-
escaped_changes = json.dumps(changes).replace("'", "\\'").replace('"', '\\"')
|
|
212
|
-
|
|
213
|
-
html = """
|
|
214
|
-
<html>
|
|
215
|
-
<head>
|
|
216
|
-
<title>Confirm """ + object_type + """ Update</title>
|
|
217
|
-
<meta charset="utf-8">
|
|
218
|
-
<style>
|
|
219
|
-
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1000px; margin: 0 auto; }
|
|
220
|
-
.warning { color: #d73a49; margin: 20px 0; padding: 15px; border-left: 4px solid #d73a49; background-color: #fff8f8; }
|
|
221
|
-
.changes { background: #f6f8fa; padding: 15px; border-radius: 6px; }
|
|
222
|
-
.buttons { margin-top: 20px; }
|
|
223
|
-
button { padding: 10px 20px; margin-right: 10px; border-radius: 6px; cursor: pointer; }
|
|
224
|
-
.approve { background: #2ea44f; color: white; border: none; }
|
|
225
|
-
.cancel { background: #d73a49; color: white; border: none; }
|
|
226
|
-
.diff-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
|
227
|
-
.diff-table td { padding: 8px; border: 1px solid #ddd; }
|
|
228
|
-
.diff-table .header { background: #f1f8ff; font-weight: bold; }
|
|
229
|
-
.status { padding: 15px; margin-top: 20px; border-radius: 6px; display: none; }
|
|
230
|
-
.success { background-color: #e6ffed; border: 1px solid #2ea44f; color: #22863a; }
|
|
231
|
-
.error { background-color: #ffeef0; border: 1px solid #d73a49; color: #d73a49; }
|
|
232
|
-
pre { white-space: pre-wrap; word-break: break-all; }
|
|
233
|
-
</style>
|
|
234
|
-
</head>
|
|
235
|
-
<body>
|
|
236
|
-
<h1>Confirm """ + object_type + """ Update</h1>
|
|
237
|
-
<p>You are about to update """ + object_type + """: <strong>""" + object_id + """</strong></p>
|
|
238
|
-
|
|
239
|
-
<div class="warning">
|
|
240
|
-
<p><strong>Warning:</strong> This action will directly update your """ + object_type.lower() + """ in Meta Ads. Please review the changes carefully before approving.</p>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div class="changes">
|
|
244
|
-
<h3>Changes to apply:</h3>
|
|
245
|
-
<table class="diff-table">
|
|
246
|
-
<tr class="header">
|
|
247
|
-
<td>Field</td>
|
|
248
|
-
<td>New Value</td>
|
|
249
|
-
<td>Description</td>
|
|
250
|
-
</tr>
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
# Generate table rows for each change
|
|
254
|
-
for k, v in changes_dict.items():
|
|
255
|
-
description = ""
|
|
256
|
-
if k == "frequency_control_specs" and isinstance(v, list) and len(v) > 0:
|
|
257
|
-
spec = v[0]
|
|
258
|
-
if all(key in spec for key in ["event", "interval_days", "max_frequency"]):
|
|
259
|
-
description = f"Cap to {spec['max_frequency']} {spec['event'].lower()} per {spec['interval_days']} days"
|
|
260
|
-
|
|
261
|
-
# Special handling for targeting_automation
|
|
262
|
-
elif k == "targeting" and isinstance(v, dict) and "targeting_automation" in v:
|
|
263
|
-
targeting_auto = v.get("targeting_automation", {})
|
|
264
|
-
if "advantage_audience" in targeting_auto:
|
|
265
|
-
audience_value = targeting_auto["advantage_audience"]
|
|
266
|
-
description = f"Set Advantage+ audience to {'ON' if audience_value == 1 else 'OFF'}"
|
|
267
|
-
if audience_value == 1:
|
|
268
|
-
description += " (may be restricted for Special Ad Categories)"
|
|
269
|
-
|
|
270
|
-
# Format the value for display
|
|
271
|
-
display_value = json.dumps(v, indent=2) if isinstance(v, (dict, list)) else str(v)
|
|
272
|
-
|
|
273
|
-
html += f"""
|
|
274
|
-
<tr>
|
|
275
|
-
<td>{k}</td>
|
|
276
|
-
<td><pre>{display_value}</pre></td>
|
|
277
|
-
<td>{description}</td>
|
|
278
|
-
</tr>
|
|
279
|
-
"""
|
|
280
|
-
|
|
281
|
-
html += """
|
|
282
|
-
</table>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
<div class="buttons">
|
|
286
|
-
<button class="approve" onclick="approveChanges()">Approve Changes</button>
|
|
287
|
-
<button class="cancel" onclick="cancelChanges()">Cancel</button>
|
|
288
|
-
</div>
|
|
289
|
-
|
|
290
|
-
<div id="status" class="status"></div>
|
|
291
|
-
|
|
292
|
-
<script>
|
|
293
|
-
// Get the approve button
|
|
294
|
-
const approveBtn = document.querySelector('.approve');
|
|
295
|
-
const cancelBtn = document.querySelector('.cancel');
|
|
296
|
-
const statusDiv = document.querySelector('.status');
|
|
297
|
-
|
|
298
|
-
let debugMode = false;
|
|
299
|
-
// Function to log debug messages
|
|
300
|
-
function debugLog(...args) {
|
|
301
|
-
if (debugMode) {
|
|
302
|
-
console.log(...args);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Add event listeners to buttons
|
|
307
|
-
approveBtn.addEventListener('click', approveChanges);
|
|
308
|
-
cancelBtn.addEventListener('click', cancelChanges);
|
|
309
|
-
|
|
310
|
-
function showStatus(message, isError = false) {
|
|
311
|
-
statusDiv.textContent = message;
|
|
312
|
-
statusDiv.style.display = 'block';
|
|
313
|
-
statusDiv.className = 'status ' + (isError ? 'error' : 'success');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function approveChanges() {
|
|
317
|
-
// Disable buttons to prevent double-submission
|
|
318
|
-
const buttons = [approveBtn, cancelBtn];
|
|
319
|
-
buttons.forEach(button => button.disabled = true);
|
|
320
|
-
|
|
321
|
-
showStatus('Approving changes...');
|
|
322
|
-
|
|
323
|
-
// Determine which object type we're updating
|
|
324
|
-
const isAd = Boolean('""" + ad_id + """');
|
|
325
|
-
const objectType = isAd ? 'ad' : 'adset';
|
|
326
|
-
const objectId = '""" + object_id + """';
|
|
327
|
-
|
|
328
|
-
// Create parameters
|
|
329
|
-
const params = new URLSearchParams({
|
|
330
|
-
action: 'approve',
|
|
331
|
-
token: '""" + token + """',
|
|
332
|
-
changes: '""" + escaped_changes + """'
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Add the appropriate ID parameter based on object type
|
|
336
|
-
if (isAd) {
|
|
337
|
-
params.append('ad_id', objectId);
|
|
338
|
-
} else {
|
|
339
|
-
params.append('adset_id', objectId);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
debugLog("Sending update request with params", {
|
|
343
|
-
objectType,
|
|
344
|
-
objectId,
|
|
345
|
-
changes: JSON.parse('""" + escaped_changes + """')
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
fetch('/update-confirm?' + params)
|
|
349
|
-
.then(response => response.json())
|
|
350
|
-
.then(data => {
|
|
351
|
-
debugLog("Update response data", data);
|
|
352
|
-
buttons.forEach(button => button.disabled = false);
|
|
353
|
-
|
|
354
|
-
// Handle result
|
|
355
|
-
if (data.status === 'error') {
|
|
356
|
-
let errorMessage = data.error || 'Unknown error';
|
|
357
|
-
const originalErrorDetails = data.api_error || {};
|
|
358
|
-
|
|
359
|
-
showStatus('Error: ' + errorMessage, true);
|
|
360
|
-
|
|
361
|
-
// Create a detailed error object for the verification page
|
|
362
|
-
const fullErrorData = {
|
|
363
|
-
message: errorMessage,
|
|
364
|
-
details: data.errorDetails || [],
|
|
365
|
-
apiError: originalErrorDetails || data.apiError || {},
|
|
366
|
-
fullResponse: data.fullResponse || {}
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
debugLog("Redirecting with error message", errorMessage);
|
|
370
|
-
debugLog("Full error data", fullErrorData);
|
|
371
|
-
|
|
372
|
-
// Encode the stringified error object
|
|
373
|
-
const encodedErrorData = encodeURIComponent(JSON.stringify(fullErrorData));
|
|
374
|
-
|
|
375
|
-
// Redirect to verification page with detailed error information
|
|
376
|
-
const errorParams = new URLSearchParams({
|
|
377
|
-
token: '""" + token + """',
|
|
378
|
-
error: errorMessage,
|
|
379
|
-
errorData: encodedErrorData
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Add the appropriate ID parameter based on object type
|
|
383
|
-
if (isAd) {
|
|
384
|
-
errorParams.append('ad_id', objectId);
|
|
385
|
-
} else {
|
|
386
|
-
errorParams.append('adset_id', objectId);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
window.location.href = '/verify-update?' + errorParams;
|
|
390
|
-
} else {
|
|
391
|
-
showStatus('Changes approved and will be applied shortly!');
|
|
392
|
-
setTimeout(() => {
|
|
393
|
-
const verifyParams = new URLSearchParams({
|
|
394
|
-
token: '""" + token + """'
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Add the appropriate ID parameter based on object type
|
|
398
|
-
if (isAd) {
|
|
399
|
-
verifyParams.append('ad_id', objectId);
|
|
400
|
-
} else {
|
|
401
|
-
verifyParams.append('adset_id', objectId);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
window.location.href = '/verify-update?' + verifyParams;
|
|
405
|
-
}, 3000);
|
|
406
|
-
}
|
|
407
|
-
})
|
|
408
|
-
.catch(error => {
|
|
409
|
-
debugLog("Fetch error", error);
|
|
410
|
-
showStatus('Error applying changes: ' + error, true);
|
|
411
|
-
buttons.forEach(button => button.disabled = false);
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function cancelChanges() {
|
|
416
|
-
showStatus("Cancelling update...");
|
|
417
|
-
|
|
418
|
-
// Determine which object type we're updating
|
|
419
|
-
const isAd = Boolean('""" + ad_id + """');
|
|
420
|
-
const objectId = '""" + object_id + """';
|
|
421
|
-
|
|
422
|
-
// Create parameters
|
|
423
|
-
const params = new URLSearchParams({
|
|
424
|
-
action: 'cancel'
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Add the appropriate ID parameter based on object type
|
|
428
|
-
if (isAd) {
|
|
429
|
-
params.append('ad_id', objectId);
|
|
430
|
-
} else {
|
|
431
|
-
params.append('adset_id', objectId);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
fetch('/update-confirm?' + params)
|
|
435
|
-
.then(() => {
|
|
436
|
-
showStatus('Update cancelled.');
|
|
437
|
-
setTimeout(() => window.close(), 2000);
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
</script>
|
|
441
|
-
</body>
|
|
442
|
-
</html>
|
|
443
|
-
"""
|
|
444
|
-
self.wfile.write(html.encode('utf-8'))
|
|
445
|
-
|
|
446
|
-
def _handle_update_execution(self):
|
|
447
|
-
"""Handle the update execution after user confirmation"""
|
|
448
|
-
# Handle update confirmation response
|
|
449
|
-
query = parse_qs(urlparse(self.path).query)
|
|
450
|
-
action = query.get("action", [""])[0]
|
|
451
|
-
|
|
452
|
-
self.send_response(200)
|
|
453
|
-
self.send_header("Content-type", "application/json")
|
|
63
|
+
self.send_header("Content-type", "text/html")
|
|
454
64
|
self.end_headers()
|
|
455
65
|
|
|
456
|
-
if
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
66
|
+
if error:
|
|
67
|
+
# User denied access or other error occurred
|
|
68
|
+
html = f"""
|
|
69
|
+
<html>
|
|
70
|
+
<head><title>Authorization Failed</title></head>
|
|
71
|
+
<body>
|
|
72
|
+
<h1>Authorization Failed</h1>
|
|
73
|
+
<p>Error: {error}</p>
|
|
74
|
+
<p>The authorization was cancelled or failed. You can close this window.</p>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
"""
|
|
78
|
+
logger.error(f"OAuth authorization failed: {error}")
|
|
79
|
+
elif code:
|
|
80
|
+
# Success case - we have the authorization code
|
|
81
|
+
logger.info(f"Received authorization code: {code[:10]}...")
|
|
82
|
+
|
|
83
|
+
# Store the authorization code temporarily
|
|
84
|
+
# The auth module will exchange this for an access token
|
|
85
|
+
token_container.update({
|
|
86
|
+
"auth_code": code,
|
|
87
|
+
"state": state,
|
|
88
|
+
"timestamp": asyncio.get_event_loop().time()
|
|
474
89
|
})
|
|
475
90
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
decoded_changes = changes
|
|
492
|
-
# Try multiple decode attempts to handle various encoding scenarios
|
|
493
|
-
for _ in range(3): # Try up to 3 levels of decoding
|
|
494
|
-
try:
|
|
495
|
-
# Try to parse as JSON
|
|
496
|
-
changes_obj = json.loads(decoded_changes)
|
|
497
|
-
# If we got a string back, we need to decode again
|
|
498
|
-
if isinstance(changes_obj, str):
|
|
499
|
-
decoded_changes = changes_obj
|
|
500
|
-
continue
|
|
501
|
-
else:
|
|
502
|
-
# We have a dictionary, break the loop
|
|
503
|
-
changes_dict = changes_obj
|
|
504
|
-
break
|
|
505
|
-
except json.JSONDecodeError:
|
|
506
|
-
# Try unescaping first
|
|
507
|
-
import html
|
|
508
|
-
decoded_changes = html.unescape(decoded_changes)
|
|
509
|
-
try:
|
|
510
|
-
changes_dict = json.loads(decoded_changes)
|
|
511
|
-
break
|
|
512
|
-
except:
|
|
513
|
-
# Failed to parse, will try again in the next iteration
|
|
514
|
-
pass
|
|
515
|
-
else:
|
|
516
|
-
# If we got here, we couldn't parse the JSON
|
|
517
|
-
return {"status": "error", "error": f"Failed to decode changes JSON: {changes}"}
|
|
518
|
-
|
|
519
|
-
endpoint = f"{object_id}"
|
|
520
|
-
|
|
521
|
-
# Create API parameters properly
|
|
522
|
-
api_params = {}
|
|
523
|
-
|
|
524
|
-
# Add each change parameter
|
|
525
|
-
for key, value in changes_dict.items():
|
|
526
|
-
api_params[key] = value
|
|
527
|
-
|
|
528
|
-
# Add the access token
|
|
529
|
-
api_params["access_token"] = token
|
|
530
|
-
|
|
531
|
-
# Log what we're about to send
|
|
532
|
-
object_type = "ad set" if object_id.startswith("23") else "ad" # Simple heuristic based on ID prefix
|
|
533
|
-
logger.info(f"Sending update to Meta API for {object_type} {object_id}")
|
|
534
|
-
logger.info(f"Parameters: {json.dumps(api_params)}")
|
|
535
|
-
|
|
536
|
-
# Make the API request to update the object
|
|
537
|
-
result = await make_api_request(endpoint, token, api_params, method="POST")
|
|
538
|
-
|
|
539
|
-
# Log the result
|
|
540
|
-
logger.info(f"Meta API update result: {json.dumps(result) if isinstance(result, dict) else result}")
|
|
541
|
-
|
|
542
|
-
# Handle various result formats
|
|
543
|
-
if result is None:
|
|
544
|
-
logger.error("Empty response from Meta API")
|
|
545
|
-
return {"status": "error", "error": "Empty response from Meta API"}
|
|
546
|
-
|
|
547
|
-
# Check if the result contains an error
|
|
548
|
-
if isinstance(result, dict) and 'error' in result:
|
|
549
|
-
# Extract detailed error message from Meta API
|
|
550
|
-
error_obj = result['error']
|
|
551
|
-
error_msg = error_obj.get('message', 'Unknown API error')
|
|
552
|
-
detailed_error = ""
|
|
553
|
-
|
|
554
|
-
# Check for more detailed error messages
|
|
555
|
-
if 'error_user_msg' in error_obj and error_obj['error_user_msg']:
|
|
556
|
-
detailed_error = error_obj['error_user_msg']
|
|
557
|
-
logger.error(f"Meta API user-facing error message: {detailed_error}")
|
|
558
|
-
|
|
559
|
-
# Extract error data if available
|
|
560
|
-
error_specs = []
|
|
561
|
-
if 'error_data' in error_obj and isinstance(error_obj['error_data'], str):
|
|
562
|
-
try:
|
|
563
|
-
error_data = json.loads(error_obj['error_data'])
|
|
564
|
-
if 'blame_field_specs' in error_data and error_data['blame_field_specs']:
|
|
565
|
-
blame_specs = error_data['blame_field_specs']
|
|
566
|
-
if isinstance(blame_specs, list) and blame_specs:
|
|
567
|
-
if isinstance(blame_specs[0], list):
|
|
568
|
-
error_specs = [msg for msg in blame_specs[0] if msg]
|
|
569
|
-
else:
|
|
570
|
-
error_specs = [str(spec) for spec in blame_specs if spec]
|
|
571
|
-
|
|
572
|
-
if error_specs:
|
|
573
|
-
logger.error(f"Meta API blame field specs: {'; '.join(error_specs)}")
|
|
574
|
-
except Exception as e:
|
|
575
|
-
logger.error(f"Error parsing error_data: {e}")
|
|
576
|
-
|
|
577
|
-
# Construct most descriptive error message
|
|
578
|
-
if detailed_error:
|
|
579
|
-
error_msg = detailed_error
|
|
580
|
-
elif error_specs:
|
|
581
|
-
error_msg = "; ".join(error_specs)
|
|
582
|
-
|
|
583
|
-
# Log the detailed error information
|
|
584
|
-
logger.error(f"Meta API error: {error_msg}")
|
|
585
|
-
logger.error(f"Full error object: {json.dumps(error_obj)}")
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
"status": "error",
|
|
589
|
-
"error": error_msg,
|
|
590
|
-
"api_error": result['error'],
|
|
591
|
-
"detailed_errors": error_specs,
|
|
592
|
-
"full_response": result
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
# Handle string results (which might be error messages)
|
|
596
|
-
if isinstance(result, str):
|
|
597
|
-
try:
|
|
598
|
-
# Try to parse as JSON
|
|
599
|
-
result_obj = json.loads(result)
|
|
600
|
-
if isinstance(result_obj, dict) and 'error' in result_obj:
|
|
601
|
-
return {"status": "error", "error": result_obj['error'].get('message', 'Unknown API error')}
|
|
602
|
-
except:
|
|
603
|
-
# If not parseable as JSON, return as error message
|
|
604
|
-
return {"status": "error", "error": result}
|
|
605
|
-
|
|
606
|
-
# If we got here, assume success
|
|
607
|
-
return {"status": "approved", "api_result": result}
|
|
608
|
-
except Exception as e:
|
|
609
|
-
# Log the exception for debugging
|
|
610
|
-
logger.error(f"Error in perform_update: {str(e)}")
|
|
611
|
-
import traceback
|
|
612
|
-
logger.error(traceback.format_exc())
|
|
613
|
-
return {"status": "error", "error": str(e)}
|
|
614
|
-
|
|
615
|
-
def _handle_update_verification(self):
|
|
616
|
-
"""Handle the verification page for updates"""
|
|
617
|
-
query = parse_qs(urlparse(self.path).query)
|
|
618
|
-
adset_id = query.get("adset_id", [""])[0]
|
|
619
|
-
ad_id = query.get("ad_id", [""])[0]
|
|
620
|
-
object_id = query.get("object_id", [""])[0]
|
|
621
|
-
|
|
622
|
-
# For backward compatibility - use object_id if available, otherwise check for adset_id or ad_id
|
|
623
|
-
if not object_id:
|
|
624
|
-
object_id = ad_id if ad_id else adset_id
|
|
625
|
-
|
|
626
|
-
object_type = query.get("object_type", [""])[0]
|
|
627
|
-
if not object_type:
|
|
628
|
-
object_type = "Ad" if ad_id else "Ad Set"
|
|
629
|
-
|
|
630
|
-
token = query.get("token", [""])[0]
|
|
631
|
-
error_message = query.get("error", [""])[0]
|
|
632
|
-
error_data_encoded = query.get("errorData", [""])[0]
|
|
633
|
-
|
|
634
|
-
# Respond with verification page
|
|
635
|
-
self.send_response(200)
|
|
636
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
637
|
-
self.end_headers()
|
|
638
|
-
|
|
639
|
-
html = """
|
|
640
|
-
<!DOCTYPE html>
|
|
641
|
-
<html>
|
|
642
|
-
<head>
|
|
643
|
-
<title>Verification - """ + object_type + """ Update</title>
|
|
644
|
-
<meta charset="utf-8">
|
|
645
|
-
<style>
|
|
646
|
-
body {
|
|
647
|
-
font-family: Arial, sans-serif;
|
|
648
|
-
margin: 20px;
|
|
649
|
-
max-width: 1000px;
|
|
650
|
-
margin: 0 auto;
|
|
651
|
-
line-height: 1.6;
|
|
652
|
-
}
|
|
653
|
-
.header {
|
|
654
|
-
margin-bottom: 20px;
|
|
655
|
-
padding-bottom: 10px;
|
|
656
|
-
border-bottom: 1px solid #ccc;
|
|
657
|
-
}
|
|
658
|
-
.success {
|
|
659
|
-
color: #2ea44f;
|
|
660
|
-
background-color: #e6ffed;
|
|
661
|
-
padding: 15px;
|
|
662
|
-
border-radius: 6px;
|
|
663
|
-
margin: 20px 0;
|
|
664
|
-
}
|
|
665
|
-
.error {
|
|
666
|
-
color: #d73a49;
|
|
667
|
-
background-color: #ffeef0;
|
|
668
|
-
padding: 15px;
|
|
669
|
-
border-radius: 6px;
|
|
670
|
-
margin: 20px 0;
|
|
671
|
-
}
|
|
672
|
-
.details {
|
|
673
|
-
background: #f6f8fa;
|
|
674
|
-
padding: 15px;
|
|
675
|
-
border-radius: 6px;
|
|
676
|
-
margin: 20px 0;
|
|
677
|
-
}
|
|
678
|
-
.btn {
|
|
679
|
-
display: inline-block;
|
|
680
|
-
padding: 10px 20px;
|
|
681
|
-
background-color: #0366d6;
|
|
682
|
-
color: white;
|
|
683
|
-
border-radius: 6px;
|
|
684
|
-
text-decoration: none;
|
|
685
|
-
margin-top: 20px;
|
|
686
|
-
}
|
|
687
|
-
pre {
|
|
688
|
-
white-space: pre-wrap;
|
|
689
|
-
word-break: break-word;
|
|
690
|
-
background: #f8f8f8;
|
|
691
|
-
padding: 10px;
|
|
692
|
-
border-radius: 4px;
|
|
693
|
-
overflow: auto;
|
|
694
|
-
}
|
|
695
|
-
#currentDetails {
|
|
696
|
-
display: none;
|
|
697
|
-
margin-top: 20px;
|
|
698
|
-
}
|
|
699
|
-
.toggle-btn {
|
|
700
|
-
background-color: #f1f8ff;
|
|
701
|
-
border: 1px solid #c8e1ff;
|
|
702
|
-
padding: 8px 16px;
|
|
703
|
-
border-radius: 4px;
|
|
704
|
-
cursor: pointer;
|
|
705
|
-
margin-top: 20px;
|
|
706
|
-
display: inline-block;
|
|
707
|
-
}
|
|
708
|
-
</style>
|
|
709
|
-
</head>
|
|
710
|
-
<body>
|
|
711
|
-
<div class="header">
|
|
712
|
-
<h1>""" + object_type + """ Update Verification</h1>
|
|
713
|
-
<p>Object ID: <strong>""" + object_id + """</strong></p>
|
|
714
|
-
</div>
|
|
715
|
-
"""
|
|
716
|
-
|
|
717
|
-
# If there's an error message, display it
|
|
718
|
-
if error_message:
|
|
719
|
-
html += """
|
|
720
|
-
<div class="error">
|
|
721
|
-
<h3>❌ Update Failed</h3>
|
|
722
|
-
<p><strong>Error:</strong> """ + error_message + """</p>
|
|
91
|
+
html = """
|
|
92
|
+
<html>
|
|
93
|
+
<head><title>Authorization Successful</title></head>
|
|
94
|
+
<body>
|
|
95
|
+
<h1>✅ Authorization Successful!</h1>
|
|
96
|
+
<p>You have successfully authorized the Meta Ads MCP application.</p>
|
|
97
|
+
<p>You can now close this window and return to your application.</p>
|
|
98
|
+
<script>
|
|
99
|
+
// Try to close the window automatically after 2 seconds
|
|
100
|
+
setTimeout(function() {
|
|
101
|
+
window.close();
|
|
102
|
+
}, 2000);
|
|
103
|
+
</script>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
723
106
|
"""
|
|
724
|
-
|
|
725
|
-
# If there's detailed error data, decode and display it
|
|
726
|
-
if error_data_encoded:
|
|
727
|
-
try:
|
|
728
|
-
error_data = json.loads(urllib.parse.unquote(error_data_encoded))
|
|
729
|
-
html += """
|
|
730
|
-
<div class="details">
|
|
731
|
-
<h4>Error Details:</h4>
|
|
732
|
-
<pre>""" + json.dumps(error_data, indent=2) + """</pre>
|
|
733
|
-
</div>
|
|
734
|
-
"""
|
|
735
|
-
except:
|
|
736
|
-
pass
|
|
737
|
-
|
|
738
|
-
html += """</div>"""
|
|
107
|
+
logger.info("OAuth authorization successful")
|
|
739
108
|
else:
|
|
740
|
-
# No error
|
|
741
|
-
html
|
|
742
|
-
<
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
109
|
+
# No code or error - something unexpected happened
|
|
110
|
+
html = """
|
|
111
|
+
<html>
|
|
112
|
+
<head><title>Unexpected Response</title></head>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>Unexpected Response</h1>
|
|
115
|
+
<p>No authorization code or error received. Please try again.</p>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
|
746
118
|
"""
|
|
119
|
+
logger.warning("OAuth callback received without code or error")
|
|
747
120
|
|
|
748
|
-
|
|
749
|
-
html += """
|
|
750
|
-
<button class="toggle-btn" id="toggleDetails">Show Current Details</button>
|
|
751
|
-
<div id="currentDetails">
|
|
752
|
-
<h3>Current """ + object_type + """ Details:</h3>
|
|
753
|
-
<pre id="detailsJson">Loading...</pre>
|
|
754
|
-
</div>
|
|
755
|
-
|
|
756
|
-
<a href="#" class="btn" onclick="window.close()">Close Window</a>
|
|
757
|
-
|
|
758
|
-
<script>
|
|
759
|
-
document.getElementById('toggleDetails').addEventListener('click', function() {
|
|
760
|
-
const detailsDiv = document.getElementById('currentDetails');
|
|
761
|
-
const toggleBtn = document.getElementById('toggleDetails');
|
|
762
|
-
|
|
763
|
-
if (detailsDiv.style.display === 'block') {
|
|
764
|
-
detailsDiv.style.display = 'none';
|
|
765
|
-
toggleBtn.textContent = 'Show Current Details';
|
|
766
|
-
} else {
|
|
767
|
-
detailsDiv.style.display = 'block';
|
|
768
|
-
toggleBtn.textContent = 'Hide Current Details';
|
|
769
|
-
|
|
770
|
-
// Fetch current details if not already loaded
|
|
771
|
-
if (document.getElementById('detailsJson').textContent === 'Loading...') {
|
|
772
|
-
fetchCurrentDetails();
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
function fetchCurrentDetails() {
|
|
778
|
-
const objectId = '""" + object_id + """';
|
|
779
|
-
const objectType = '""" + object_type.lower() + """';
|
|
780
|
-
const endpoint = objectType === 'ad' ?
|
|
781
|
-
`/api/ad?ad_id=${objectId}&token=""" + token + """` :
|
|
782
|
-
`/api/adset?adset_id=${objectId}&token=""" + token + """`;
|
|
783
|
-
|
|
784
|
-
fetch(endpoint)
|
|
785
|
-
.then(response => response.json())
|
|
786
|
-
.then(data => {
|
|
787
|
-
document.getElementById('detailsJson').textContent = JSON.stringify(data, null, 2);
|
|
788
|
-
})
|
|
789
|
-
.catch(error => {
|
|
790
|
-
document.getElementById('detailsJson').textContent = `Error fetching details: ${error}`;
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
</script>
|
|
794
|
-
</body>
|
|
795
|
-
</html>
|
|
796
|
-
"""
|
|
797
|
-
|
|
798
|
-
self.wfile.write(html.encode('utf-8'))
|
|
121
|
+
self.wfile.write(html.encode())
|
|
799
122
|
|
|
800
|
-
def
|
|
801
|
-
"""Handle
|
|
802
|
-
#
|
|
803
|
-
|
|
804
|
-
adset_id = query.get("adset_id", [""])[0]
|
|
805
|
-
token = query.get("token", [""])[0]
|
|
806
|
-
|
|
807
|
-
from .api import make_api_request
|
|
808
|
-
|
|
809
|
-
# Call the Graph API directly
|
|
810
|
-
async def get_adset_data():
|
|
811
|
-
try:
|
|
812
|
-
endpoint = f"{adset_id}"
|
|
813
|
-
params = {
|
|
814
|
-
"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"
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
result = await make_api_request(endpoint, token, params)
|
|
818
|
-
|
|
819
|
-
# Check if result is a string (possibly an error message)
|
|
820
|
-
if isinstance(result, str):
|
|
821
|
-
try:
|
|
822
|
-
# Try to parse as JSON
|
|
823
|
-
parsed_result = json.loads(result)
|
|
824
|
-
return parsed_result
|
|
825
|
-
except json.JSONDecodeError:
|
|
826
|
-
# Return error object if can't parse as JSON
|
|
827
|
-
return {"error": {"message": result}}
|
|
828
|
-
|
|
829
|
-
# If the result is None, return an error object
|
|
830
|
-
if result is None:
|
|
831
|
-
return {"error": {"message": "Empty response from API"}}
|
|
832
|
-
|
|
833
|
-
return result
|
|
834
|
-
except Exception as e:
|
|
835
|
-
logger.error(f"Error in get_adset_data: {str(e)}")
|
|
836
|
-
return {"error": {"message": f"Error fetching ad set data: {str(e)}"}}
|
|
837
|
-
|
|
838
|
-
# Run the async function
|
|
839
|
-
loop = asyncio.new_event_loop()
|
|
840
|
-
asyncio.set_event_loop(loop)
|
|
841
|
-
result = loop.run_until_complete(get_adset_data())
|
|
842
|
-
loop.close()
|
|
123
|
+
def _handle_token(self):
|
|
124
|
+
"""Handle token endpoint for retrieving stored token data"""
|
|
125
|
+
# This endpoint allows other parts of the application to retrieve
|
|
126
|
+
# token information from the callback server
|
|
843
127
|
|
|
844
|
-
# Return the result
|
|
845
128
|
self.send_response(200)
|
|
846
129
|
self.send_header("Content-type", "application/json")
|
|
847
130
|
self.end_headers()
|
|
848
|
-
self.wfile.write(json.dumps(result, indent=2).encode())
|
|
849
|
-
|
|
850
|
-
def _handle_ad_api(self):
|
|
851
|
-
"""Handle API requests for ad data"""
|
|
852
|
-
# Parse query parameters
|
|
853
|
-
query = parse_qs(urlparse(self.path).query)
|
|
854
|
-
ad_id = query.get("ad_id", [""])[0]
|
|
855
|
-
token = query.get("token", [""])[0]
|
|
856
|
-
|
|
857
|
-
from .api import make_api_request
|
|
858
131
|
|
|
859
|
-
#
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
return await make_api_request(endpoint, token, params)
|
|
132
|
+
# Return current token container contents
|
|
133
|
+
response_data = {
|
|
134
|
+
"status": "success",
|
|
135
|
+
"data": token_container
|
|
136
|
+
}
|
|
866
137
|
|
|
867
|
-
|
|
868
|
-
result = asyncio.run(get_ad_data())
|
|
138
|
+
self.wfile.write(json.dumps(response_data).encode())
|
|
869
139
|
|
|
870
|
-
#
|
|
871
|
-
|
|
872
|
-
self.send_header("Content-type", "application/json")
|
|
873
|
-
self.end_headers()
|
|
874
|
-
|
|
875
|
-
if isinstance(result, dict):
|
|
876
|
-
self.wfile.write(json.dumps(result, indent=2).encode())
|
|
877
|
-
else:
|
|
878
|
-
self.wfile.write(json.dumps({"error": "Failed to get ad data"}, indent=2).encode())
|
|
140
|
+
# The actual token processing is now handled by the auth module
|
|
141
|
+
# that imports this module and accesses token_container
|
|
879
142
|
|
|
880
143
|
# Silence server logs
|
|
881
144
|
def log_message(self, format, *args):
|
|
@@ -897,125 +160,98 @@ def shutdown_callback_server():
|
|
|
897
160
|
server_shutdown_timer.cancel()
|
|
898
161
|
server_shutdown_timer = None
|
|
899
162
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if callback_server_instance:
|
|
904
|
-
try:
|
|
163
|
+
try:
|
|
164
|
+
if callback_server_instance:
|
|
165
|
+
print("Shutting down callback server...")
|
|
905
166
|
callback_server_instance.shutdown()
|
|
906
|
-
callback_server_instance
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
167
|
+
callback_server_instance.server_close()
|
|
168
|
+
print("Callback server shut down successfully")
|
|
169
|
+
|
|
170
|
+
if callback_server_thread and callback_server_thread.is_alive():
|
|
171
|
+
callback_server_thread.join(timeout=5)
|
|
172
|
+
if callback_server_thread.is_alive():
|
|
173
|
+
print("Warning: Callback server thread did not shut down cleanly")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"Error during callback server shutdown: {e}")
|
|
176
|
+
finally:
|
|
177
|
+
callback_server_running = False
|
|
178
|
+
callback_server_thread = None
|
|
179
|
+
callback_server_port = None
|
|
180
|
+
callback_server_instance = None
|
|
181
|
+
|
|
913
182
|
|
|
914
183
|
def start_callback_server() -> int:
|
|
915
184
|
"""
|
|
916
|
-
Start the callback server
|
|
185
|
+
Start the callback server and return the port number it's running on.
|
|
917
186
|
|
|
918
187
|
Returns:
|
|
919
|
-
Port number the server is
|
|
920
|
-
|
|
188
|
+
int: Port number the server is listening on
|
|
189
|
+
|
|
921
190
|
Raises:
|
|
922
|
-
Exception: If
|
|
191
|
+
Exception: If the server fails to start
|
|
923
192
|
"""
|
|
924
|
-
# Check if callback server is disabled via environment variable
|
|
925
|
-
if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
|
|
926
|
-
logger.info("Callback server disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
|
|
927
|
-
print("Callback server is disabled. OAuth authentication flow cannot be used.")
|
|
928
|
-
raise Exception("Callback server disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable. Use alternative authentication methods.")
|
|
929
|
-
|
|
930
193
|
global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
|
|
931
194
|
|
|
195
|
+
# Check if callback server is disabled
|
|
196
|
+
if os.environ.get("META_ADS_DISABLE_CALLBACK_SERVER"):
|
|
197
|
+
raise Exception("Callback server is disabled via META_ADS_DISABLE_CALLBACK_SERVER environment variable")
|
|
198
|
+
|
|
932
199
|
with callback_server_lock:
|
|
933
200
|
if callback_server_running:
|
|
934
201
|
print(f"Callback server already running on port {callback_server_port}")
|
|
935
|
-
|
|
936
|
-
# Reset the shutdown timer if one exists
|
|
937
|
-
if server_shutdown_timer is not None:
|
|
938
|
-
server_shutdown_timer.cancel()
|
|
939
|
-
|
|
940
|
-
server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
|
|
941
|
-
server_shutdown_timer.daemon = True
|
|
942
|
-
server_shutdown_timer.start()
|
|
943
|
-
print(f"Reset server shutdown timer to {CALLBACK_SERVER_TIMEOUT} seconds")
|
|
944
|
-
|
|
945
202
|
return callback_server_port
|
|
946
203
|
|
|
947
204
|
# Find an available port
|
|
948
|
-
port =
|
|
205
|
+
port = 8080
|
|
949
206
|
max_attempts = 10
|
|
950
207
|
for attempt in range(max_attempts):
|
|
951
208
|
try:
|
|
209
|
+
# Test if port is available
|
|
952
210
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
953
211
|
s.bind(('localhost', port))
|
|
954
|
-
|
|
212
|
+
break
|
|
955
213
|
except OSError:
|
|
956
214
|
port += 1
|
|
957
|
-
|
|
958
|
-
|
|
215
|
+
else:
|
|
216
|
+
raise Exception(f"Could not find an available port after {max_attempts} attempts")
|
|
959
217
|
|
|
960
218
|
callback_server_port = port
|
|
961
219
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
server_shutdown_timer.daemon = True
|
|
1001
|
-
server_shutdown_timer.start()
|
|
1002
|
-
print(f"Server will automatically shut down after {CALLBACK_SERVER_TIMEOUT} seconds of inactivity")
|
|
1003
|
-
|
|
1004
|
-
# Verify the server is actually accepting connections
|
|
1005
|
-
try:
|
|
1006
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1007
|
-
s.settimeout(2)
|
|
1008
|
-
s.connect(('localhost', port))
|
|
1009
|
-
print(f"Confirmed server is accepting connections on port {port}")
|
|
1010
|
-
except Exception as e:
|
|
1011
|
-
print(f"Warning: Could not verify server connection: {e}")
|
|
1012
|
-
|
|
1013
|
-
return port
|
|
1014
|
-
|
|
1015
|
-
except Exception as e:
|
|
1016
|
-
print(f"Error starting callback server: {e}")
|
|
1017
|
-
# Try again with a different port in case of bind issues
|
|
1018
|
-
if "address already in use" in str(e).lower():
|
|
1019
|
-
print("Port may be in use, trying a different port...")
|
|
1020
|
-
return start_callback_server() # Recursive call with a new port
|
|
1021
|
-
raise e
|
|
220
|
+
# Start the server in a separate thread
|
|
221
|
+
callback_server_thread = threading.Thread(target=server_thread, daemon=True)
|
|
222
|
+
callback_server_thread.start()
|
|
223
|
+
|
|
224
|
+
# Wait a moment for the server to start
|
|
225
|
+
import time
|
|
226
|
+
time.sleep(0.5)
|
|
227
|
+
|
|
228
|
+
if not callback_server_running:
|
|
229
|
+
raise Exception("Failed to start callback server")
|
|
230
|
+
|
|
231
|
+
# Set up automatic shutdown timer
|
|
232
|
+
def auto_shutdown():
|
|
233
|
+
print(f"Callback server auto-shutdown after {CALLBACK_SERVER_TIMEOUT} seconds")
|
|
234
|
+
shutdown_callback_server()
|
|
235
|
+
|
|
236
|
+
server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, auto_shutdown)
|
|
237
|
+
server_shutdown_timer.start()
|
|
238
|
+
|
|
239
|
+
print(f"Callback server started on http://localhost:{port}")
|
|
240
|
+
return port
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def server_thread():
|
|
244
|
+
"""Thread function to run the callback server"""
|
|
245
|
+
global callback_server_running, callback_server_instance
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
callback_server_instance = HTTPServer(('localhost', callback_server_port), CallbackHandler)
|
|
249
|
+
callback_server_running = True
|
|
250
|
+
print(f"Callback server thread started on port {callback_server_port}")
|
|
251
|
+
callback_server_instance.serve_forever()
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Callback server error: {e}")
|
|
254
|
+
callback_server_running = False
|
|
255
|
+
finally:
|
|
256
|
+
print("Callback server thread finished")
|
|
257
|
+
callback_server_running = False
|