medicafe 0.250810.6__py3-none-any.whl → 0.250811.0__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.
@@ -16,6 +16,20 @@ setup_python_path()
16
16
 
17
17
  from MediCafe.core_utils import get_shared_config_loader
18
18
 
19
+ # New helpers
20
+ from MediLink.gmail_oauth_utils import (
21
+ get_authorization_url as oauth_get_authorization_url,
22
+ exchange_code_for_token as oauth_exchange_code_for_token,
23
+ refresh_access_token as oauth_refresh_access_token,
24
+ is_valid_authorization_code as oauth_is_valid_authorization_code,
25
+ clear_token_cache as oauth_clear_token_cache,
26
+ )
27
+ from MediLink.gmail_http_utils import (
28
+ generate_self_signed_cert as http_generate_self_signed_cert,
29
+ start_https_server as http_start_https_server,
30
+ inspect_token as http_inspect_token,
31
+ )
32
+
19
33
  # Get shared config loader
20
34
  MediLink_ConfigLoader = get_shared_config_loader()
21
35
  if MediLink_ConfigLoader:
@@ -94,77 +108,29 @@ log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
94
108
  REDIRECT_URI = 'https://127.0.0.1:8000'
95
109
 
96
110
  def get_authorization_url():
97
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
98
- credentials = json.load(credentials_file)
99
- client_id = credentials['web']['client_id']
100
- auth_url = (
101
- "https://accounts.google.com/o/oauth2/v2/auth?"
102
- "response_type=code&"
103
- "client_id={}&"
104
- "redirect_uri={}&"
105
- "scope={}&"
106
- "access_type=offline&" # Requesting offline access allows the application to obtain a refresh token, enabling it to access resources even when the user is not actively using the app. This is useful for long-lived sessions.
107
- # To improve user experience, consider changing this to 'online' if you don't need offline access:
108
- # "access_type=online&" # Use this if you only need access while the user is actively using the app and don't require a refresh token.
109
-
110
- "prompt=consent" # This forces the user to re-consent to the requested scopes every time they authenticate. While this is useful for ensuring the user is aware of the permissions being granted, it can be modified to 'none' or omitted entirely if the application is functioning correctly and tokens are being refreshed properly.
111
- # To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
112
- # "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
113
- # Alternatively, you can omit the prompt parameter entirely to use the default behavior:
114
- # # "prompt=" # Omitting this will show the consent screen only when necessary.
115
- ).format(client_id, REDIRECT_URI, SCOPES)
116
- log("Generated authorization URL: {}".format(auth_url))
117
- return auth_url
111
+ return oauth_get_authorization_url(CREDENTIALS_PATH, REDIRECT_URI, SCOPES, log)
118
112
 
119
113
  def exchange_code_for_token(auth_code, retries=3):
120
- for attempt in range(retries):
121
- try:
122
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
123
- credentials = json.load(credentials_file)
124
- token_url = "https://oauth2.googleapis.com/token"
125
- data = {
126
- 'code': auth_code,
127
- 'client_id': credentials['web']['client_id'],
128
- 'client_secret': credentials['web']['client_secret'],
129
- 'redirect_uri': REDIRECT_URI,
130
- 'grant_type': 'authorization_code'
131
- }
132
- response = requests.post(token_url, data=data)
133
- log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
134
- token_response = response.json()
135
- if response.status_code == 200:
136
- token_response['token_time'] = time.time()
137
- return token_response
138
- else:
139
- log("Token exchange failed: {}".format(token_response))
140
- if attempt < retries - 1:
141
- log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
142
- except Exception as e:
143
- log("Error during token exchange: {}".format(e))
144
- return {}
114
+ return oauth_exchange_code_for_token(auth_code, CREDENTIALS_PATH, REDIRECT_URI, log, retries=retries)
145
115
 
146
116
  def get_access_token():
147
117
  if os.path.exists(TOKEN_PATH):
148
118
  with open(TOKEN_PATH, 'r') as token_file:
149
119
  token_data = json.load(token_file)
150
120
  log("Loaded token data:\n {}".format(token_data))
151
-
152
121
  if 'access_token' in token_data and 'expires_in' in token_data:
153
122
  try:
154
- # Use current time if 'token_time' is missing
155
123
  token_time = token_data.get('token_time', time.time())
156
124
  token_expiry_time = token_time + token_data['expires_in']
157
-
158
125
  except KeyError as e:
159
126
  log("KeyError while accessing token data: {}".format(e))
160
127
  return None
161
-
162
128
  if token_expiry_time > time.time():
163
129
  log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
164
130
  return token_data['access_token']
165
131
  else:
166
132
  log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
167
- new_token_data = refresh_access_token(token_data.get('refresh_token'))
133
+ new_token_data = oauth_refresh_access_token(token_data.get('refresh_token'), CREDENTIALS_PATH, log)
168
134
  if 'access_token' in new_token_data:
169
135
  new_token_data['token_time'] = time.time()
170
136
  with open(TOKEN_PATH, 'w') as token_file:
@@ -178,44 +144,20 @@ def get_access_token():
178
144
  return None
179
145
 
180
146
  def refresh_access_token(refresh_token):
181
- log("Refreshing access token.")
182
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
183
- credentials = json.load(credentials_file)
184
- token_url = "https://oauth2.googleapis.com/token"
185
- data = {
186
- 'client_id': credentials['web']['client_id'],
187
- 'client_secret': credentials['web']['client_secret'],
188
- 'refresh_token': refresh_token,
189
- 'grant_type': 'refresh_token'
190
- }
191
- response = requests.post(token_url, data=data)
192
- log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
193
- if response.status_code == 200:
194
- log("Access token refreshed successfully.")
195
- return response.json()
196
- else:
197
- log("Failed to refresh access token. Status code: {}".format(response.status_code))
198
- return {}
147
+ return oauth_refresh_access_token(refresh_token, CREDENTIALS_PATH, log)
199
148
 
200
149
  def bring_window_to_foreground():
201
150
  """Brings the current window to the foreground on Windows."""
202
151
  try:
203
152
  if platform.system() == 'Windows':
204
- # Get the current process ID
205
153
  pid = os.getpid()
206
- # Get the window handle for the current process
207
154
  hwnd = ctypes.windll.user32.GetForegroundWindow()
208
- # Get the process ID of the window
209
155
  current_pid = ctypes.c_ulong()
210
156
  ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(current_pid))
211
-
212
- # If the window is not ours, try to bring it to front
213
157
  if current_pid.value != pid:
214
- # Try to set the window to foreground
215
158
  ctypes.windll.user32.SetForegroundWindow(hwnd)
216
- # If that fails, try the alternative method
217
159
  if ctypes.windll.user32.GetForegroundWindow() != hwnd:
218
- ctypes.windll.user32.ShowWindow(hwnd, 9) # SW_RESTORE = 9
160
+ ctypes.windll.user32.ShowWindow(hwnd, 9)
219
161
  ctypes.windll.user32.SetForegroundWindow(hwnd)
220
162
  except Exception as e:
221
163
  log("Error bringing window to foreground: {}".format(e))
@@ -238,22 +180,17 @@ class RequestHandler(BaseHTTPRequestHandler):
238
180
  post_data = self.rfile.read(content_length)
239
181
  data = json.loads(post_data.decode('utf-8'))
240
182
  links = data.get('links', [])
241
-
242
- # Log the content of links
243
183
  log("Received links: {}".format(links))
244
-
245
184
  file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
246
185
  log("File IDs received from client: {}".format(file_ids))
247
-
248
- # Proceed with downloading files
249
186
  download_docx_files(links)
250
187
  self.send_response(200)
251
- self._set_headers() # Include CORS headers
188
+ self._set_headers()
252
189
  self.end_headers()
253
190
  response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
254
191
  self.wfile.write(response.encode('utf-8'))
255
192
  shutdown_event.set()
256
- bring_window_to_foreground() # Bring window to foreground after download
193
+ bring_window_to_foreground()
257
194
  elif self.path == '/shutdown':
258
195
  log("Shutdown request received.")
259
196
  self.send_response(200)
@@ -261,14 +198,13 @@ class RequestHandler(BaseHTTPRequestHandler):
261
198
  self.end_headers()
262
199
  response = json.dumps({"status": "success", "message": "Server is shutting down."})
263
200
  self.wfile.write(response.encode('utf-8'))
264
- shutdown_event.set() # Signal shutdown event instead of calling stop_server directly
201
+ shutdown_event.set()
265
202
  elif self.path == '/delete-files':
266
203
  content_length = int(self.headers['Content-Length'])
267
204
  post_data = self.rfile.read(content_length)
268
205
  data = json.loads(post_data.decode('utf-8'))
269
206
  file_ids = data.get('fileIds', [])
270
207
  log("File IDs to delete received from client: {}".format(file_ids))
271
-
272
208
  if not isinstance(file_ids, list):
273
209
  self.send_response(400)
274
210
  self._set_headers()
@@ -276,9 +212,8 @@ class RequestHandler(BaseHTTPRequestHandler):
276
212
  response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
277
213
  self.wfile.write(response.encode('utf-8'))
278
214
  return
279
-
280
215
  self.send_response(200)
281
- self._set_headers() # Include CORS headers
216
+ self._set_headers()
282
217
  self.end_headers()
283
218
  response = json.dumps({"status": "success", "message": "Files deleted successfully."})
284
219
  self.wfile.write(response.encode('utf-8'))
@@ -287,23 +222,21 @@ class RequestHandler(BaseHTTPRequestHandler):
287
222
  self.end_headers()
288
223
 
289
224
  def do_GET(self):
290
- log("Full request path: {}".format(self.path)) # Log the full path for debugging
225
+ log("Full request path: {}".format(self.path))
291
226
  if self.path.startswith("/?code="):
292
227
  auth_code = self.path.split('=')[1].split('&')[0]
293
- auth_code = requests.utils.unquote(auth_code) # Decode if URL-encoded
228
+ auth_code = requests.utils.unquote(auth_code)
294
229
  log("Received authorization code: {}".format(auth_code))
295
- if is_valid_authorization_code(auth_code):
230
+ if oauth_is_valid_authorization_code(auth_code, log):
296
231
  try:
297
232
  token_response = exchange_code_for_token(auth_code)
298
233
  if 'access_token' not in token_response:
299
- # Check for specific error message
300
234
  if token_response.get("status") == "error":
301
235
  self.send_response(400)
302
236
  self.send_header('Content-type', 'text/html')
303
237
  self.end_headers()
304
238
  self.wfile.write(token_response["message"].encode())
305
239
  return
306
- # Handle other cases
307
240
  raise ValueError("Access token not found in response.")
308
241
  except Exception as e:
309
242
  log("Error during token exchange: {}".format(e))
@@ -312,7 +245,7 @@ class RequestHandler(BaseHTTPRequestHandler):
312
245
  self.end_headers()
313
246
  self.wfile.write("An error occurred during authentication. Please try again.".encode())
314
247
  else:
315
- log("Token response: {}".format(token_response)) # Add this line
248
+ log("Token response: {}".format(token_response))
316
249
  if 'access_token' in token_response:
317
250
  with open(TOKEN_PATH, 'w') as token_file:
318
251
  json.dump(token_response, token_file)
@@ -320,34 +253,31 @@ class RequestHandler(BaseHTTPRequestHandler):
320
253
  self.send_header('Content-type', 'text/html')
321
254
  self.end_headers()
322
255
  self.wfile.write("Authentication successful. You can close this window now.".encode())
323
- initiate_link_retrieval(config) # Pass config here
256
+ initiate_link_retrieval(config)
324
257
  else:
325
- log("Authentication failed with response: {}".format(token_response)) # Log the full response
258
+ log("Authentication failed with response: {}".format(token_response))
326
259
  if 'error' in token_response:
327
260
  error_description = token_response.get('error_description', 'No description provided.')
328
- log("Error details: {}".format(error_description)) # Log specific error details
329
-
330
- # Provide user feedback based on the error
261
+ log("Error details: {}".format(error_description))
331
262
  if token_response.get('error') == 'invalid_grant':
332
263
  log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
333
264
  check_invalid_grant_causes(auth_code)
334
- clear_token_cache() # Clear the cache on invalid grant
265
+ oauth_clear_token_cache(TOKEN_PATH, log)
335
266
  user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
336
267
  else:
337
268
  user_message = "Authentication failed. Please check the logs for more details."
338
-
339
269
  self.send_response(400)
340
270
  self.send_header('Content-type', 'text/html')
341
271
  self.end_headers()
342
272
  self.wfile.write(user_message.encode())
343
- shutdown_event.set() # Signal shutdown event after failed authentication
273
+ shutdown_event.set()
344
274
  else:
345
275
  log("Invalid authorization code format: {}".format(auth_code))
346
276
  self.send_response(400)
347
277
  self.send_header('Content-type', 'text/html')
348
278
  self.end_headers()
349
279
  self.wfile.write("Invalid authorization code format. Please try again.".encode())
350
- shutdown_event.set() # Signal shutdown event after failed authentication
280
+ shutdown_event.set()
351
281
  elif self.path == '/downloaded-emails':
352
282
  self.send_response(200)
353
283
  self._set_headers()
@@ -365,75 +295,17 @@ class RequestHandler(BaseHTTPRequestHandler):
365
295
  self.wfile.write(b'HTTPS server is running.')
366
296
 
367
297
  def generate_self_signed_cert(cert_file, key_file):
368
- log("Checking if certificate file exists: " + cert_file)
369
- log("Checking if key file exists: " + key_file)
370
-
371
- # Check if certificate exists and is not expired
372
- cert_needs_regeneration = True
373
- if os.path.exists(cert_file):
374
- try:
375
- # Check certificate expiration
376
- check_cmd = ['openssl', 'x509', '-in', cert_file, '-checkend', '86400', '-noout'] # Check if expires in next 24 hours
377
- result = subprocess.call(check_cmd)
378
- if result == 0:
379
- log("Certificate is still valid")
380
- cert_needs_regeneration = False
381
- else:
382
- log("Certificate is expired or will expire soon")
383
- # Delete expired certificate and key files
384
- try:
385
- if os.path.exists(cert_file):
386
- os.remove(cert_file)
387
- log("Deleted expired certificate file: {}".format(cert_file))
388
- if os.path.exists(key_file):
389
- os.remove(key_file)
390
- log("Deleted expired key file: {}".format(key_file))
391
- except Exception as e:
392
- log("Error deleting expired certificate files: {}".format(e))
393
- except Exception as e:
394
- log("Error checking certificate expiration: {}".format(e))
395
-
396
- if cert_needs_regeneration:
397
- log("Generating self-signed SSL certificate...")
398
- cmd = [
399
- 'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
400
- '-keyout', key_file,
401
- '-out', cert_file,
402
- '-days', '365',
403
- '-sha256' # Use SHA-256 for better security
404
- #'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
405
- ]
406
- try:
407
- log("Running command: " + ' '.join(cmd))
408
- result = subprocess.call(cmd)
409
- log("Command finished with result: " + str(result))
410
- if result != 0:
411
- raise RuntimeError("Failed to generate self-signed certificate")
412
-
413
- # Verify the certificate was generated correctly
414
- verify_cmd = ['openssl', 'x509', '-in', cert_file, '-text', '-noout']
415
- verify_result = subprocess.call(verify_cmd)
416
- if verify_result != 0:
417
- raise RuntimeError("Generated certificate verification failed")
418
-
419
- log("Self-signed SSL certificate generated and verified successfully.")
420
- except Exception as e:
421
- log("Error generating self-signed certificate: {}".format(e))
422
- raise
298
+ http_generate_self_signed_cert(openssl_cnf, cert_file, key_file, log, subprocess)
423
299
 
424
300
  def run_server():
425
301
  global httpd
426
302
  try:
427
303
  log("Attempting to start server on port " + str(server_port))
428
- server_address = ('0.0.0.0', server_port) # Bind to all interfaces
429
- httpd = HTTPServer(server_address, RequestHandler)
430
- log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
431
-
432
304
  if not os.path.exists(cert_file):
433
305
  log("Error: Certificate file not found: " + cert_file)
434
306
  if not os.path.exists(key_file):
435
307
  log("Error: Key file not found: " + key_file)
436
-
308
+ httpd = HTTPServer(('0.0.0.0', server_port), RequestHandler)
437
309
  httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
438
310
  log("Starting HTTPS server on port {}".format(server_port))
439
311
  httpd.serve_forever()
@@ -448,8 +320,8 @@ def stop_server():
448
320
  httpd.shutdown()
449
321
  httpd.server_close()
450
322
  log("HTTPS server stopped.")
451
- shutdown_event.set() # Signal shutdown event
452
- bring_window_to_foreground() # Bring window to foreground after shutdown
323
+ shutdown_event.set()
324
+ bring_window_to_foreground()
453
325
 
454
326
  def load_downloaded_emails():
455
327
  downloaded_emails = set()
@@ -460,86 +332,26 @@ def load_downloaded_emails():
460
332
  return downloaded_emails
461
333
 
462
334
  def download_docx_files(links):
463
- # Load the set of downloaded emails
464
- # TODO (LOW-MEDIUM PRIORITY - CSV File Detection and Routing):
465
- # PROBLEM: Downloaded files may include CSV files that need special handling and routing.
466
- # Currently all files are treated the same regardless of extension.
467
- #
468
- # IMPLEMENTATION REQUIREMENTS:
469
- # 1. File Extension Detection:
470
- # - Check each downloaded file for .csv extension (case-insensitive)
471
- # - Also check for common CSV variants: .txt, .tsv, .dat (based on content)
472
- # - Handle files with multiple extensions like "report.csv.zip"
473
- #
474
- # 2. Content-Based Detection (Advanced):
475
- # - For files without clear extensions, peek at content
476
- # - Look for CSV patterns: comma-separated values, consistent column counts
477
- # - Handle Excel files that might be CSV exports (.xlsx with CSV content)
478
- #
479
- # 3. CSV Routing Logic:
480
- # - Move CSV files to dedicated CSV processing directory
481
- # - Maintain file naming conventions for downstream processing
482
- # - Log CSV file movements for audit trail
483
- # - Preserve original file permissions and timestamps
484
- #
485
- # IMPLEMENTATION STEPS:
486
- # 1. Add helper function detect_csv_files(downloaded_files) -> list
487
- # 2. Add helper function move_csv_to_processing_dir(csv_file, destination_dir)
488
- # 3. Add configuration for CSV destination directory in config file
489
- # 4. Update this function to call CSV detection and routing after download
490
- # 5. Add error handling for file movement failures
491
- # 6. Add logging for all CSV file operations
492
- #
493
- # CONFIGURATION NEEDED:
494
- # - config['csv_processing_dir']: Where to move detected CSV files
495
- # - config['csv_file_extensions']: List of extensions to treat as CSV
496
- # - config['csv_content_detection']: Boolean to enable content-based detection
497
- #
498
- # ERROR HANDLING:
499
- # - Handle permission errors when moving files
500
- # - Handle disk space issues
501
- # - Gracefully handle corrupted or locked files
502
- # - Provide fallback options when CSV directory is unavailable
503
- #
504
- # TESTING SCENARIOS:
505
- # - Mixed file types: .docx, .csv, .pdf in same download batch
506
- # - CSV files with unusual extensions (.txt, .dat)
507
- # - Large CSV files (>100MB)
508
- # - CSV files in ZIP archives
509
- #
510
- # FILES TO MODIFY: This file (download_docx_files function)
511
- # RELATED: May need updates to CSV processing modules that expect files in specific locations
512
335
  downloaded_emails = load_downloaded_emails()
513
-
514
336
  for link in links:
515
337
  try:
516
338
  url = link.get('url', '')
517
339
  filename = link.get('filename', '')
518
-
519
- # Log the variables to debug
520
340
  log("Processing link: url='{}', filename='{}'".format(url, filename))
521
-
522
- # CSV ROUTING PLACEHOLDER:
523
- # - Detect CSV-like extensions before download and log the intended routing decision.
524
- # - This is a no-op for now to avoid side-effects until the pipeline is verified on XP.
525
341
  lower_name = (filename or '').lower()
526
342
  looks_like_csv = any(lower_name.endswith(ext) for ext in ['.csv', '.tsv', '.txt', '.dat'])
527
343
  if looks_like_csv:
528
344
  log("[CSV Routing Preview] Detected CSV-like filename: {}. Would route to CSV processing directory.".format(filename))
529
-
530
- # Skip if email already downloaded
531
345
  if filename in downloaded_emails:
532
346
  log("Skipping already downloaded email: {}".format(filename))
533
347
  continue
534
-
535
348
  log("Downloading .docx file from URL: {}".format(url))
536
- response = requests.get(url, verify=False) # Set verify to False for self-signed certs
349
+ response = requests.get(url, verify=False)
537
350
  if response.status_code == 200:
538
351
  file_path = os.path.join(local_storage_path, filename)
539
352
  with open(file_path, 'wb') as file:
540
353
  file.write(response.content)
541
354
  log("Downloaded .docx file: {}".format(filename))
542
- # Add to the set and save the updated list
543
355
  downloaded_emails.add(filename)
544
356
  with open(downloaded_emails_file, 'a') as file:
545
357
  file.write(filename + '\n')
@@ -567,59 +379,44 @@ def open_browser_with_executable(url, browser_path=None):
567
379
 
568
380
  def initiate_link_retrieval(config):
569
381
  log("Initiating browser via implicit GET.")
570
- url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
382
+ url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id'])
571
383
  open_browser_with_executable(url_get)
572
-
573
384
  log("Preparing POST call.")
574
- url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
385
+ url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id'])
575
386
  downloaded_emails = list(load_downloaded_emails())
576
- payload = {
577
- "downloadedEmails": downloaded_emails
578
- }
579
-
387
+ payload = {"downloadedEmails": downloaded_emails}
580
388
  access_token = get_access_token()
581
389
  if not access_token:
582
390
  log("Access token not found. Please authenticate first.")
583
- shutdown_event.set() # Signal shutdown event if token is not found
391
+ shutdown_event.set()
584
392
  return
585
-
586
- # Inspect the token to check its validity and permissions
587
- token_info = inspect_token(access_token)
393
+ token_info = http_inspect_token(access_token, log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
588
394
  if token_info is None:
589
395
  log("Access token is invalid. Please re-authenticate.")
590
- shutdown_event.set() # Signal shutdown event if token is invalid
396
+ shutdown_event.set()
591
397
  return
592
-
593
- # Proceed with the rest of the function if the token is valid
594
- headers = {
595
- 'Authorization': 'Bearer {}'.format(access_token),
596
- 'Content-Type': 'application/json'
597
- }
598
-
398
+ headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
599
399
  log("Request headers: {}".format(headers))
600
400
  log("Request payload: {}".format(payload))
601
-
602
401
  handle_post_response(url, payload, headers)
603
-
402
+
604
403
  def handle_post_response(url, payload, headers):
605
404
  try:
606
405
  response = requests.post(url, json=payload, headers=headers)
607
406
  log("Response status code: {}".format(response.status_code))
608
407
  log("Response body: {}".format(response.text))
609
-
610
408
  if response.status_code == 200:
611
409
  response_data = response.json()
612
- log("Parsed response data: {}".format(response_data)) # Log the parsed response data
410
+ log("Parsed response data: {}".format(response_data))
613
411
  if response_data.get("status") == "error":
614
412
  log("Error message from server: {}".format(response_data.get("message")))
615
413
  print("Error: {}".format(response_data.get("message")))
616
- shutdown_event.set() # Signal shutdown event after error
414
+ shutdown_event.set()
617
415
  else:
618
416
  log("Link retrieval initiated successfully.")
619
417
  elif response.status_code == 401:
620
418
  log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
621
- # Inspect the token to log its details
622
- token_info = inspect_token(headers['Authorization'].split(' ')[1])
419
+ token_info = http_inspect_token(headers['Authorization'].split(' ')[1], log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
623
420
  log("Token details: {}".format(token_info))
624
421
  shutdown_event.set()
625
422
  elif response.status_code == 403:
@@ -639,26 +436,7 @@ def handle_post_response(url, payload, headers):
639
436
  shutdown_event.set()
640
437
 
641
438
  def inspect_token(access_token):
642
- info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
643
- try:
644
- response = requests.get(info_url)
645
- log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
646
-
647
- if response.status_code == 200:
648
- return response.json()
649
- else:
650
- log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
651
- # Check for invalid token
652
- if response.status_code == 400 and "invalid_token" in response.text:
653
- log("Access token is invalid. Deleting token.json and stopping the server.")
654
- delete_token_file() # Delete the token.json file
655
- print("Access token is invalid. Please re-authenticate and restart the server.")
656
- stop_server() # Stop the server
657
- return None # Return None for invalid tokens
658
- return None # Return None for other invalid tokens
659
- except Exception as e:
660
- log("Exception during token inspection: {}".format(e))
661
- return None
439
+ return http_inspect_token(access_token, log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
662
440
 
663
441
  def delete_token_file():
664
442
  try:
@@ -679,67 +457,38 @@ def auth_and_retrieval():
679
457
  access_token = get_access_token()
680
458
  if not access_token:
681
459
  log("Access token not found or expired. Please authenticate first.")
682
- #print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
683
460
  auth_url = get_authorization_url()
684
- #print(auth_url)
685
461
  open_browser_with_executable(auth_url)
686
- shutdown_event.wait() # Wait for the shutdown event to be set after authentication
462
+ shutdown_event.wait()
687
463
  else:
688
464
  log("Access token found. Proceeding.")
689
- initiate_link_retrieval(config) # Pass config here
690
- shutdown_event.wait() # Wait for the shutdown event to be set
465
+ initiate_link_retrieval(config)
466
+ shutdown_event.wait()
691
467
 
692
468
  def is_valid_authorization_code(auth_code):
693
- # Check if the authorization code is not None and is a non-empty string
694
- if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
695
- return True
696
- log("Invalid authorization code format: {}".format(auth_code))
697
- return False
469
+ return oauth_is_valid_authorization_code(auth_code, log)
698
470
 
699
471
  def clear_token_cache():
700
- if os.path.exists(TOKEN_PATH):
701
- os.remove(TOKEN_PATH)
702
- log("Cleared token cache.")
472
+ oauth_clear_token_cache(TOKEN_PATH, log)
703
473
 
704
474
  def check_invalid_grant_causes(auth_code):
705
- # TODO Implement this function in the future to check for common causes of invalid_grant error
706
- # Log potential causes for invalid_grant
707
- # XP/Network NOTE: On older systems, clock skew and reused codes are frequent causes.
708
475
  log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
709
- # Suggested checks (to be implemented when plumbing is ready):
710
- # - Has authorization code already been used?
711
- # - Does redirect URI exactly match the one registered (case and trailing slashes)?
712
- # - Is system clock skewed? Compare to Google time; log skew if detected.
713
- # - Are the requested scopes enabled for this OAuth client?
714
- # - Did the user revoke access between code issuance and token exchange?
715
- # Each of these would produce a specific log to speed up troubleshooting on XP.
716
- """
717
- if is_code_used(auth_code):
718
- log("Authorization code has already been used.")
719
- if not is_redirect_uri_correct():
720
- log("Redirect URI does not match the registered URI.")
721
- """
722
476
 
723
477
  if __name__ == "__main__":
724
478
  signal.signal(signal.SIGINT, signal_handler)
725
479
  signal.signal(signal.SIGTERM, signal_handler)
726
-
727
480
  try:
728
- # Generate SSL certificate if it doesn't exist
729
481
  generate_self_signed_cert(cert_file, key_file)
730
-
731
482
  from threading import Thread
732
483
  log("Starting server thread.")
733
484
  server_thread = Thread(target=run_server)
734
485
  server_thread.daemon = True
735
486
  server_thread.start()
736
-
737
487
  auth_and_retrieval()
738
-
739
488
  log("Stopping HTTPS server.")
740
- stop_server() # Ensure the server is stopped
489
+ stop_server()
741
490
  log("Waiting for server thread to finish.")
742
- server_thread.join() # Wait for the server thread to finish
491
+ server_thread.join()
743
492
  except KeyboardInterrupt:
744
493
  log("KeyboardInterrupt received, stopping server.")
745
494
  stop_server()