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