medicafe 0.250410.0__py3-none-any.whl → 0.250430.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.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

@@ -1,581 +1,608 @@
1
- # MediLink_Gmail.py
2
- import sys, os, subprocess, time, webbrowser, requests, json, ssl, signal
3
- from MediLink_ConfigLoader import log, load_configuration
4
- from http.server import BaseHTTPRequestHandler, HTTPServer
5
- from threading import Thread, Event
6
- import platform
7
-
8
- config, _ = load_configuration()
9
- local_storage_path = config['MediLink_Config']['local_storage_path']
10
- downloaded_emails_file = os.path.join(local_storage_path, 'downloaded_emails.txt')
11
-
12
- server_port = 8000
13
- cert_file = 'server.cert'
14
- key_file = 'server.key'
15
- openssl_cnf = 'MediLink\\openssl.cnf' # This file needs to be located in the same place as where MediCafe is run from (so MediBot folder?)
16
-
17
- httpd = None # Global variable for the HTTP server
18
- shutdown_event = Event() # Event to signal shutdown
19
-
20
- # Define the scopes for the Gmail API and other required APIs
21
- SCOPES = ' '.join([
22
- 'https://www.googleapis.com/auth/gmail.modify',
23
- 'https://www.googleapis.com/auth/gmail.compose',
24
- 'https://www.googleapis.com/auth/gmail.readonly',
25
- 'https://www.googleapis.com/auth/script.external_request',
26
- 'https://www.googleapis.com/auth/userinfo.email',
27
- 'https://www.googleapis.com/auth/script.scriptapp',
28
- 'https://www.googleapis.com/auth/drive'
29
- ])
30
-
31
- # Path to token.json file
32
- TOKEN_PATH = 'token.json'
33
-
34
- # Determine the operating system and version
35
- os_name = platform.system()
36
- os_version = platform.release()
37
-
38
- # Set the credentials path based on the OS and version
39
- if os_name == 'Windows' and 'XP' in os_version:
40
- CREDENTIALS_PATH = 'F:\\Medibot\\json\\credentials.json'
41
- else:
42
- CREDENTIALS_PATH = 'json\\credentials.json'
43
-
44
- # Log the selected path for verification
45
- log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
46
-
47
- REDIRECT_URI = 'https://127.0.0.1:8000'
48
-
49
- def get_authorization_url():
50
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
51
- credentials = json.load(credentials_file)
52
- client_id = credentials['web']['client_id']
53
- auth_url = (
54
- "https://accounts.google.com/o/oauth2/v2/auth?"
55
- "response_type=code&"
56
- "client_id={}&"
57
- "redirect_uri={}&"
58
- "scope={}&"
59
- "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.
60
- # To improve user experience, consider changing this to 'online' if you don't need offline access:
61
- # "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.
62
-
63
- "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.
64
- # To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
65
- # "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
66
- # Alternatively, you can omit the prompt parameter entirely to use the default behavior:
67
- # # "prompt=" # Omitting this will show the consent screen only when necessary.
68
- ).format(client_id, REDIRECT_URI, SCOPES)
69
- log("Generated authorization URL: {}".format(auth_url))
70
- return auth_url
71
-
72
- def exchange_code_for_token(auth_code, retries=3):
73
- for attempt in range(retries):
74
- try:
75
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
76
- credentials = json.load(credentials_file)
77
- token_url = "https://oauth2.googleapis.com/token"
78
- data = {
79
- 'code': auth_code,
80
- 'client_id': credentials['web']['client_id'],
81
- 'client_secret': credentials['web']['client_secret'],
82
- 'redirect_uri': REDIRECT_URI,
83
- 'grant_type': 'authorization_code'
84
- }
85
- response = requests.post(token_url, data=data)
86
- log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
87
- token_response = response.json()
88
- if response.status_code == 200:
89
- token_response['token_time'] = time.time()
90
- return token_response
91
- else:
92
- log("Token exchange failed: {}".format(token_response))
93
- if attempt < retries - 1:
94
- log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
95
- except Exception as e:
96
- log("Error during token exchange: {}".format(e))
97
- return {}
98
-
99
- def get_access_token():
100
- if os.path.exists(TOKEN_PATH):
101
- with open(TOKEN_PATH, 'r') as token_file:
102
- token_data = json.load(token_file)
103
- log("Loaded token data:\n {}".format(token_data))
104
-
105
- if 'access_token' in token_data and 'expires_in' in token_data:
106
- try:
107
- # Use current time if 'token_time' is missing
108
- token_time = token_data.get('token_time', time.time())
109
- token_expiry_time = token_time + token_data['expires_in']
110
-
111
- except KeyError as e:
112
- log("KeyError while accessing token data: {}".format(e))
113
- return None
114
-
115
- if token_expiry_time > time.time():
116
- log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
117
- return token_data['access_token']
118
- else:
119
- log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
120
- new_token_data = refresh_access_token(token_data.get('refresh_token'))
121
- if 'access_token' in new_token_data:
122
- new_token_data['token_time'] = time.time()
123
- with open(TOKEN_PATH, 'w') as token_file:
124
- json.dump(new_token_data, token_file)
125
- log("Access token refreshed successfully. New token data: {}".format(new_token_data))
126
- return new_token_data['access_token']
127
- else:
128
- log("Failed to refresh access token. New token data: {}".format(new_token_data))
129
- return None
130
- log("Access token not found. Please authenticate.")
131
- return None
132
-
133
- def refresh_access_token(refresh_token):
134
- log("Refreshing access token.")
135
- with open(CREDENTIALS_PATH, 'r') as credentials_file:
136
- credentials = json.load(credentials_file)
137
- token_url = "https://oauth2.googleapis.com/token"
138
- data = {
139
- 'client_id': credentials['web']['client_id'],
140
- 'client_secret': credentials['web']['client_secret'],
141
- 'refresh_token': refresh_token,
142
- 'grant_type': 'refresh_token'
143
- }
144
- response = requests.post(token_url, data=data)
145
- log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
146
- if response.status_code == 200:
147
- log("Access token refreshed successfully.")
148
- return response.json()
149
- else:
150
- log("Failed to refresh access token. Status code: {}".format(response.status_code))
151
- return {}
152
- class RequestHandler(BaseHTTPRequestHandler):
153
- def _set_headers(self):
154
- self.send_header('Access-Control-Allow-Origin', '*')
155
- self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
156
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
157
- self.send_header('Content-type', 'application/json')
158
-
159
- def do_OPTIONS(self):
160
- self.send_response(200)
161
- self._set_headers()
162
- self.end_headers()
163
-
164
- def do_POST(self):
165
- if self.path == '/download':
166
- content_length = int(self.headers['Content-Length'])
167
- post_data = self.rfile.read(content_length)
168
- data = json.loads(post_data.decode('utf-8'))
169
- links = data.get('links', [])
170
-
171
- # Log the content of links
172
- log("Received links: {}".format(links))
173
-
174
- file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
175
- log("File IDs received from client: {}".format(file_ids))
176
-
177
- # Proceed with downloading files
178
- download_docx_files(links)
179
- self.send_response(200)
180
- self._set_headers() # Include CORS headers
181
- self.end_headers()
182
- response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
183
- self.wfile.write(response.encode('utf-8'))
184
- shutdown_event.set()
185
- elif self.path == '/shutdown':
186
- log("Shutdown request received.")
187
- self.send_response(200)
188
- self._set_headers()
189
- self.end_headers()
190
- response = json.dumps({"status": "success", "message": "Server is shutting down."})
191
- self.wfile.write(response.encode('utf-8'))
192
- shutdown_event.set() # Signal shutdown event instead of calling stop_server directly
193
- elif self.path == '/delete-files':
194
- content_length = int(self.headers['Content-Length'])
195
- post_data = self.rfile.read(content_length)
196
- data = json.loads(post_data.decode('utf-8'))
197
- file_ids = data.get('fileIds', [])
198
- log("File IDs to delete received from client: {}".format(file_ids))
199
-
200
- if not isinstance(file_ids, list):
201
- self.send_response(400)
202
- self._set_headers()
203
- self.end_headers()
204
- response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
205
- self.wfile.write(response.encode('utf-8'))
206
- return
207
-
208
- self.send_response(200)
209
- self._set_headers() # Include CORS headers
210
- self.end_headers()
211
- response = json.dumps({"status": "success", "message": "Files deleted successfully."})
212
- self.wfile.write(response.encode('utf-8'))
213
- else:
214
- self.send_response(404)
215
- self.end_headers()
216
-
217
- def do_GET(self):
218
- log("Full request path: {}".format(self.path)) # Log the full path for debugging
219
- if self.path.startswith("/?code="):
220
- auth_code = self.path.split('=')[1].split('&')[0]
221
- auth_code = requests.utils.unquote(auth_code) # Decode if URL-encoded
222
- log("Received authorization code: {}".format(auth_code))
223
- if is_valid_authorization_code(auth_code):
224
- try:
225
- token_response = exchange_code_for_token(auth_code)
226
- if 'access_token' not in token_response:
227
- # Check for specific error message
228
- if token_response.get("status") == "error":
229
- self.send_response(400)
230
- self.send_header('Content-type', 'text/html')
231
- self.end_headers()
232
- self.wfile.write(token_response["message"].encode())
233
- return
234
- # Handle other cases
235
- raise ValueError("Access token not found in response.")
236
- except Exception as e:
237
- log("Error during token exchange: {}".format(e))
238
- self.send_response(500)
239
- self.send_header('Content-type', 'text/html')
240
- self.end_headers()
241
- self.wfile.write("An error occurred during authentication. Please try again.".encode())
242
- else:
243
- log("Token response: {}".format(token_response)) # Add this line
244
- if 'access_token' in token_response:
245
- with open(TOKEN_PATH, 'w') as token_file:
246
- json.dump(token_response, token_file)
247
- self.send_response(200)
248
- self.send_header('Content-type', 'text/html')
249
- self.end_headers()
250
- self.wfile.write("Authentication successful. You can close this window now.".encode())
251
- initiate_link_retrieval(config) # Pass config here
252
- else:
253
- log("Authentication failed with response: {}".format(token_response)) # Log the full response
254
- if 'error' in token_response:
255
- error_description = token_response.get('error_description', 'No description provided.')
256
- log("Error details: {}".format(error_description)) # Log specific error details
257
-
258
- # Provide user feedback based on the error
259
- if token_response.get('error') == 'invalid_grant':
260
- log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
261
- check_invalid_grant_causes(auth_code)
262
- clear_token_cache() # Clear the cache on invalid grant
263
- user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
264
- else:
265
- user_message = "Authentication failed. Please check the logs for more details."
266
-
267
- self.send_response(400)
268
- self.send_header('Content-type', 'text/html')
269
- self.end_headers()
270
- self.wfile.write(user_message.encode())
271
- shutdown_event.set() # Signal shutdown event after failed authentication
272
- else:
273
- log("Invalid authorization code format: {}".format(auth_code))
274
- self.send_response(400)
275
- self.send_header('Content-type', 'text/html')
276
- self.end_headers()
277
- self.wfile.write("Invalid authorization code format. Please try again.".encode())
278
- shutdown_event.set() # Signal shutdown event after failed authentication
279
- elif self.path == '/downloaded-emails':
280
- self.send_response(200)
281
- self._set_headers()
282
- self.end_headers()
283
- downloaded_emails = load_downloaded_emails()
284
- response = json.dumps({"downloadedEmails": list(downloaded_emails)})
285
- self.wfile.write(response.encode('utf-8'))
286
- else:
287
- self.send_response(200)
288
- self.send_header('Access-Control-Allow-Origin', '*')
289
- self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
290
- self.send_header('Access-Control-Allow-Headers', 'Content-Type')
291
- self.send_header('Content-type', 'text/html')
292
- self.end_headers()
293
- self.wfile.write(b'HTTPS server is running.')
294
-
295
- def generate_self_signed_cert(cert_file, key_file):
296
- log("Checking if certificate file exists: " + cert_file)
297
- log("Checking if key file exists: " + key_file)
298
- if not os.path.exists(cert_file) or not os.path.exists(key_file):
299
- log("Generating self-signed SSL certificate...")
300
- cmd = [
301
- 'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
302
- '-keyout', key_file,
303
- '-out', cert_file,
304
- '-days', '365'
305
- #'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
306
- ]
307
- try:
308
- log("Running command: " + ' '.join(cmd))
309
- result = subprocess.call(cmd)
310
- log("Command finished with result: " + str(result))
311
- if result != 0:
312
- raise RuntimeError("Failed to generate self-signed certificate")
313
- log("Self-signed SSL certificate generated.")
314
- except Exception as e:
315
- log("Error generating self-signed certificate: {}".format(e))
316
- raise
317
-
318
- def run_server():
319
- global httpd
320
- try:
321
- log("Attempting to start server on port " + str(server_port))
322
- server_address = ('0.0.0.0', server_port) # Bind to all interfaces
323
- httpd = HTTPServer(server_address, RequestHandler)
324
- log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
325
-
326
- if not os.path.exists(cert_file):
327
- log("Error: Certificate file not found: " + cert_file)
328
- if not os.path.exists(key_file):
329
- log("Error: Key file not found: " + key_file)
330
-
331
- httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
332
- log("Starting HTTPS server on port {}".format(server_port))
333
- httpd.serve_forever()
334
- except Exception as e:
335
- log("Error in serving: {}".format(e))
336
- stop_server()
337
-
338
- def stop_server():
339
- global httpd
340
- if httpd:
341
- log("Stopping HTTPS server.")
342
- httpd.shutdown()
343
- httpd.server_close()
344
- log("HTTPS server stopped.")
345
- shutdown_event.set() # Signal shutdown event
346
-
347
- def load_downloaded_emails():
348
- downloaded_emails = set()
349
- if os.path.exists(downloaded_emails_file):
350
- with open(downloaded_emails_file, 'r') as file:
351
- downloaded_emails = set(line.strip() for line in file)
352
- log("Loaded downloaded emails: {}".format(downloaded_emails))
353
- return downloaded_emails
354
-
355
- def download_docx_files(links):
356
- # Load the set of downloaded emails
357
- # TODO Test if any of these have a .csv extension and then move those to the right location locally.
358
- downloaded_emails = load_downloaded_emails()
359
-
360
- for link in links:
361
- try:
362
- url = link.get('url', '')
363
- filename = link.get('filename', '')
364
-
365
- # Log the variables to debug
366
- log("Processing link: url='{}', filename='{}'".format(url, filename))
367
-
368
- # Skip if email already downloaded
369
- if filename in downloaded_emails:
370
- log("Skipping already downloaded email: {}".format(filename))
371
- continue
372
-
373
- log("Downloading .docx file from URL: {}".format(url))
374
- response = requests.get(url, verify=False) # Set verify to False for self-signed certs
375
- if response.status_code == 200:
376
- file_path = os.path.join(local_storage_path, filename)
377
- with open(file_path, 'wb') as file:
378
- file.write(response.content)
379
- log("Downloaded .docx file: {}".format(filename))
380
- # Add to the set and save the updated list
381
- downloaded_emails.add(filename)
382
- with open(downloaded_emails_file, 'a') as file:
383
- file.write(filename + '\n')
384
- else:
385
- log("Failed to download .docx file from URL: {}. Status code: {}".format(url, response.status_code))
386
- except Exception as e:
387
- log("Error downloading .docx file from URL: {}. Error: {}".format(url, e))
388
-
389
- def open_browser_with_executable(url, browser_path=None):
390
- try:
391
- if browser_path:
392
- log("Attempting to open URL with provided executable: {} {}".format(browser_path, url))
393
- process = subprocess.Popen([browser_path, url], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
394
- stdout, stderr = process.communicate()
395
- if process.returncode == 0:
396
- log("Browser opened with provided executable path using subprocess.Popen.")
397
- else:
398
- log("Browser failed to open using subprocess.Popen. Return code: {}. Stderr: {}".format(process.returncode, stderr))
399
- else:
400
- log("No browser path provided. Attempting to open URL with default browser: {}".format(url))
401
- webbrowser.open(url)
402
- log("Default browser opened.")
403
- except Exception as e:
404
- log("Failed to open browser: {}".format(e))
405
-
406
- def initiate_link_retrieval(config):
407
- log("Initiating browser via implicit GET.")
408
- url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
409
- open_browser_with_executable(url_get)
410
-
411
- log("Preparing POST call.")
412
- url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
413
- downloaded_emails = list(load_downloaded_emails())
414
- payload = {
415
- "downloadedEmails": downloaded_emails
416
- }
417
-
418
- access_token = get_access_token()
419
- if not access_token:
420
- log("Access token not found. Please authenticate first.")
421
- shutdown_event.set() # Signal shutdown event if token is not found
422
- return
423
-
424
- # Inspect the token to check its validity and permissions
425
- token_info = inspect_token(access_token)
426
- if token_info is None:
427
- log("Access token is invalid. Please re-authenticate.")
428
- shutdown_event.set() # Signal shutdown event if token is invalid
429
- return
430
-
431
- # Proceed with the rest of the function if the token is valid
432
- headers = {
433
- 'Authorization': 'Bearer {}'.format(access_token),
434
- 'Content-Type': 'application/json'
435
- }
436
-
437
- log("Request headers: {}".format(headers))
438
- log("Request payload: {}".format(payload))
439
-
440
- handle_post_response(url, payload, headers)
441
-
442
- def handle_post_response(url, payload, headers):
443
- try:
444
- response = requests.post(url, json=payload, headers=headers)
445
- log("Response status code: {}".format(response.status_code))
446
- log("Response body: {}".format(response.text))
447
-
448
- if response.status_code == 200:
449
- response_data = response.json()
450
- log("Parsed response data: {}".format(response_data)) # Log the parsed response data
451
- if response_data.get("status") == "error":
452
- log("Error message from server: {}".format(response_data.get("message")))
453
- print("Error: {}".format(response_data.get("message")))
454
- shutdown_event.set() # Signal shutdown event after error
455
- else:
456
- log("Link retrieval initiated successfully.")
457
- elif response.status_code == 401:
458
- log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
459
- # Inspect the token to log its details
460
- token_info = inspect_token(headers['Authorization'].split(' ')[1])
461
- log("Token details: {}".format(token_info))
462
- shutdown_event.set()
463
- elif response.status_code == 403:
464
- log("Forbidden access. Ensure that the OAuth client has the correct permissions. Response body: {}".format(response.text))
465
- shutdown_event.set()
466
- elif response.status_code == 404:
467
- log("Not Found. Verify the URL and ensure the Apps Script is deployed correctly. Response body: {}".format(response.text))
468
- shutdown_event.set()
469
- else:
470
- log("Failed to initiate link retrieval. Unexpected status code: {}. Response body: {}".format(response.status_code, response.text))
471
- shutdown_event.set()
472
- except requests.exceptions.RequestException as e:
473
- log("RequestException during link retrieval initiation: {}".format(e))
474
- shutdown_event.set()
475
- except Exception as e:
476
- log("Unexpected error during link retrieval initiation: {}".format(e))
477
- shutdown_event.set()
478
-
479
- def inspect_token(access_token):
480
- info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
481
- try:
482
- response = requests.get(info_url)
483
- log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
484
-
485
- if response.status_code == 200:
486
- return response.json()
487
- else:
488
- log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
489
- # Check for invalid token
490
- if response.status_code == 400 and "invalid_token" in response.text:
491
- log("Access token is invalid. Deleting token.json and stopping the server.")
492
- delete_token_file() # Delete the token.json file
493
- print("Access token is invalid. Please re-authenticate and restart the server.")
494
- stop_server() # Stop the server
495
- return None # Return None for invalid tokens
496
- return None # Return None for other invalid tokens
497
- except Exception as e:
498
- log("Exception during token inspection: {}".format(e))
499
- return None
500
-
501
- def delete_token_file():
502
- try:
503
- if os.path.exists(TOKEN_PATH):
504
- os.remove(TOKEN_PATH)
505
- log("Deleted token.json successfully.")
506
- else:
507
- log("token.json does not exist.")
508
- except Exception as e:
509
- log("Error deleting token.json: {}".format(e))
510
-
511
- def signal_handler(sig, frame):
512
- log("Signal received: {}. Initiating shutdown.".format(sig))
513
- stop_server()
514
- sys.exit(0)
515
-
516
- def auth_and_retrieval():
517
- access_token = get_access_token()
518
- if not access_token:
519
- log("Access token not found or expired. Please authenticate first.")
520
- print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
521
- auth_url = get_authorization_url()
522
- print(auth_url)
523
- open_browser_with_executable(auth_url)
524
- shutdown_event.wait() # Wait for the shutdown event to be set after authentication
525
- else:
526
- log("Access token found. Proceeding.")
527
- initiate_link_retrieval(config) # Pass config here
528
- shutdown_event.wait() # Wait for the shutdown event to be set
529
-
530
- def is_valid_authorization_code(auth_code):
531
- # Check if the authorization code is not None and is a non-empty string
532
- if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
533
- return True
534
- log("Invalid authorization code format: {}".format(auth_code))
535
- return False
536
-
537
- def clear_token_cache():
538
- if os.path.exists(TOKEN_PATH):
539
- os.remove(TOKEN_PATH)
540
- log("Cleared token cache.")
541
-
542
- def check_invalid_grant_causes(auth_code):
543
- # TODO Implement this function in the future to check for common causes of invalid_grant error
544
- # Log potential causes for invalid_grant
545
- log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
546
- # Example checks (you can expand this based on your needs)
547
- """
548
- if is_code_used(auth_code):
549
- log("Authorization code has already been used.")
550
- if not is_redirect_uri_correct():
551
- log("Redirect URI does not match the registered URI.")
552
- """
553
-
554
- if __name__ == "__main__":
555
- signal.signal(signal.SIGINT, signal_handler)
556
- signal.signal(signal.SIGTERM, signal_handler)
557
-
558
- try:
559
- # Generate SSL certificate if it doesn't exist
560
- generate_self_signed_cert(cert_file, key_file)
561
-
562
- from threading import Thread
563
- log("Starting server thread.")
564
- server_thread = Thread(target=run_server)
565
- server_thread.daemon = True
566
- server_thread.start()
567
-
568
- auth_and_retrieval()
569
-
570
- log("Stopping HTTPS server.")
571
- stop_server() # Ensure the server is stopped
572
- log("Waiting for server thread to finish.")
573
- server_thread.join() # Wait for the server thread to finish
574
- except KeyboardInterrupt:
575
- log("KeyboardInterrupt received, stopping server.")
576
- stop_server()
577
- sys.exit(0)
578
- except Exception as e:
579
- log("An error occurred: {}".format(e))
580
- stop_server()
1
+ # MediLink_Gmail.py
2
+ import sys, os, subprocess, time, webbrowser, requests, json, ssl, signal
3
+ from MediLink_ConfigLoader import log, load_configuration
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from threading import Thread, Event
6
+ import platform
7
+ import ctypes
8
+
9
+ config, _ = load_configuration()
10
+ local_storage_path = config['MediLink_Config']['local_storage_path']
11
+ downloaded_emails_file = os.path.join(local_storage_path, 'downloaded_emails.txt')
12
+
13
+ server_port = 8000
14
+ cert_file = 'server.cert'
15
+ key_file = 'server.key'
16
+ openssl_cnf = 'MediLink\\openssl.cnf' # This file needs to be located in the same place as where MediCafe is run from (so MediBot folder?)
17
+
18
+ httpd = None # Global variable for the HTTP server
19
+ shutdown_event = Event() # Event to signal shutdown
20
+
21
+ # Define the scopes for the Gmail API and other required APIs
22
+ SCOPES = ' '.join([
23
+ 'https://www.googleapis.com/auth/gmail.modify',
24
+ 'https://www.googleapis.com/auth/gmail.compose',
25
+ 'https://www.googleapis.com/auth/gmail.readonly',
26
+ 'https://www.googleapis.com/auth/script.external_request',
27
+ 'https://www.googleapis.com/auth/userinfo.email',
28
+ 'https://www.googleapis.com/auth/script.scriptapp',
29
+ 'https://www.googleapis.com/auth/drive'
30
+ ])
31
+
32
+ # Path to token.json file
33
+ TOKEN_PATH = 'token.json'
34
+
35
+ # Determine the operating system and version
36
+ os_name = platform.system()
37
+ os_version = platform.release()
38
+
39
+ # Set the credentials path based on the OS and version
40
+ if os_name == 'Windows' and 'XP' in os_version:
41
+ CREDENTIALS_PATH = 'F:\\Medibot\\json\\credentials.json'
42
+ else:
43
+ CREDENTIALS_PATH = 'json\\credentials.json'
44
+
45
+ # Log the selected path for verification
46
+ log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
47
+
48
+ REDIRECT_URI = 'https://127.0.0.1:8000'
49
+
50
+ def get_authorization_url():
51
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
52
+ credentials = json.load(credentials_file)
53
+ client_id = credentials['web']['client_id']
54
+ auth_url = (
55
+ "https://accounts.google.com/o/oauth2/v2/auth?"
56
+ "response_type=code&"
57
+ "client_id={}&"
58
+ "redirect_uri={}&"
59
+ "scope={}&"
60
+ "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.
61
+ # To improve user experience, consider changing this to 'online' if you don't need offline access:
62
+ # "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.
63
+
64
+ "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.
65
+ # To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
66
+ # "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
67
+ # Alternatively, you can omit the prompt parameter entirely to use the default behavior:
68
+ # # "prompt=" # Omitting this will show the consent screen only when necessary.
69
+ ).format(client_id, REDIRECT_URI, SCOPES)
70
+ log("Generated authorization URL: {}".format(auth_url))
71
+ return auth_url
72
+
73
+ def exchange_code_for_token(auth_code, retries=3):
74
+ for attempt in range(retries):
75
+ try:
76
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
77
+ credentials = json.load(credentials_file)
78
+ token_url = "https://oauth2.googleapis.com/token"
79
+ data = {
80
+ 'code': auth_code,
81
+ 'client_id': credentials['web']['client_id'],
82
+ 'client_secret': credentials['web']['client_secret'],
83
+ 'redirect_uri': REDIRECT_URI,
84
+ 'grant_type': 'authorization_code'
85
+ }
86
+ response = requests.post(token_url, data=data)
87
+ log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
88
+ token_response = response.json()
89
+ if response.status_code == 200:
90
+ token_response['token_time'] = time.time()
91
+ return token_response
92
+ else:
93
+ log("Token exchange failed: {}".format(token_response))
94
+ if attempt < retries - 1:
95
+ log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
96
+ except Exception as e:
97
+ log("Error during token exchange: {}".format(e))
98
+ return {}
99
+
100
+ def get_access_token():
101
+ if os.path.exists(TOKEN_PATH):
102
+ with open(TOKEN_PATH, 'r') as token_file:
103
+ token_data = json.load(token_file)
104
+ log("Loaded token data:\n {}".format(token_data))
105
+
106
+ if 'access_token' in token_data and 'expires_in' in token_data:
107
+ try:
108
+ # Use current time if 'token_time' is missing
109
+ token_time = token_data.get('token_time', time.time())
110
+ token_expiry_time = token_time + token_data['expires_in']
111
+
112
+ except KeyError as e:
113
+ log("KeyError while accessing token data: {}".format(e))
114
+ return None
115
+
116
+ if token_expiry_time > time.time():
117
+ log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
118
+ return token_data['access_token']
119
+ else:
120
+ log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
121
+ new_token_data = refresh_access_token(token_data.get('refresh_token'))
122
+ if 'access_token' in new_token_data:
123
+ new_token_data['token_time'] = time.time()
124
+ with open(TOKEN_PATH, 'w') as token_file:
125
+ json.dump(new_token_data, token_file)
126
+ log("Access token refreshed successfully. New token data: {}".format(new_token_data))
127
+ return new_token_data['access_token']
128
+ else:
129
+ log("Failed to refresh access token. New token data: {}".format(new_token_data))
130
+ return None
131
+ log("Access token not found. Please authenticate.")
132
+ return None
133
+
134
+ def refresh_access_token(refresh_token):
135
+ log("Refreshing access token.")
136
+ with open(CREDENTIALS_PATH, 'r') as credentials_file:
137
+ credentials = json.load(credentials_file)
138
+ token_url = "https://oauth2.googleapis.com/token"
139
+ data = {
140
+ 'client_id': credentials['web']['client_id'],
141
+ 'client_secret': credentials['web']['client_secret'],
142
+ 'refresh_token': refresh_token,
143
+ 'grant_type': 'refresh_token'
144
+ }
145
+ response = requests.post(token_url, data=data)
146
+ log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
147
+ if response.status_code == 200:
148
+ log("Access token refreshed successfully.")
149
+ return response.json()
150
+ else:
151
+ log("Failed to refresh access token. Status code: {}".format(response.status_code))
152
+ return {}
153
+
154
+ def bring_window_to_foreground():
155
+ """Brings the current window to the foreground on Windows."""
156
+ try:
157
+ if platform.system() == 'Windows':
158
+ # Get the current process ID
159
+ pid = os.getpid()
160
+ # Get the window handle for the current process
161
+ hwnd = ctypes.windll.user32.GetForegroundWindow()
162
+ # Get the process ID of the window
163
+ current_pid = ctypes.c_ulong()
164
+ ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(current_pid))
165
+
166
+ # If the window is not ours, try to bring it to front
167
+ if current_pid.value != pid:
168
+ # Try to set the window to foreground
169
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
170
+ # If that fails, try the alternative method
171
+ if ctypes.windll.user32.GetForegroundWindow() != hwnd:
172
+ ctypes.windll.user32.ShowWindow(hwnd, 9) # SW_RESTORE = 9
173
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
174
+ except Exception as e:
175
+ log("Error bringing window to foreground: {}".format(e))
176
+
177
+ class RequestHandler(BaseHTTPRequestHandler):
178
+ def _set_headers(self):
179
+ self.send_header('Access-Control-Allow-Origin', '*')
180
+ self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
181
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
182
+ self.send_header('Content-type', 'application/json')
183
+
184
+ def do_OPTIONS(self):
185
+ self.send_response(200)
186
+ self._set_headers()
187
+ self.end_headers()
188
+
189
+ def do_POST(self):
190
+ if self.path == '/download':
191
+ content_length = int(self.headers['Content-Length'])
192
+ post_data = self.rfile.read(content_length)
193
+ data = json.loads(post_data.decode('utf-8'))
194
+ links = data.get('links', [])
195
+
196
+ # Log the content of links
197
+ log("Received links: {}".format(links))
198
+
199
+ file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
200
+ log("File IDs received from client: {}".format(file_ids))
201
+
202
+ # Proceed with downloading files
203
+ download_docx_files(links)
204
+ self.send_response(200)
205
+ self._set_headers() # Include CORS headers
206
+ self.end_headers()
207
+ response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
208
+ self.wfile.write(response.encode('utf-8'))
209
+ shutdown_event.set()
210
+ bring_window_to_foreground() # Bring window to foreground after download
211
+ elif self.path == '/shutdown':
212
+ log("Shutdown request received.")
213
+ self.send_response(200)
214
+ self._set_headers()
215
+ self.end_headers()
216
+ response = json.dumps({"status": "success", "message": "Server is shutting down."})
217
+ self.wfile.write(response.encode('utf-8'))
218
+ shutdown_event.set() # Signal shutdown event instead of calling stop_server directly
219
+ elif self.path == '/delete-files':
220
+ content_length = int(self.headers['Content-Length'])
221
+ post_data = self.rfile.read(content_length)
222
+ data = json.loads(post_data.decode('utf-8'))
223
+ file_ids = data.get('fileIds', [])
224
+ log("File IDs to delete received from client: {}".format(file_ids))
225
+
226
+ if not isinstance(file_ids, list):
227
+ self.send_response(400)
228
+ self._set_headers()
229
+ self.end_headers()
230
+ response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
231
+ self.wfile.write(response.encode('utf-8'))
232
+ return
233
+
234
+ self.send_response(200)
235
+ self._set_headers() # Include CORS headers
236
+ self.end_headers()
237
+ response = json.dumps({"status": "success", "message": "Files deleted successfully."})
238
+ self.wfile.write(response.encode('utf-8'))
239
+ else:
240
+ self.send_response(404)
241
+ self.end_headers()
242
+
243
+ def do_GET(self):
244
+ log("Full request path: {}".format(self.path)) # Log the full path for debugging
245
+ if self.path.startswith("/?code="):
246
+ auth_code = self.path.split('=')[1].split('&')[0]
247
+ auth_code = requests.utils.unquote(auth_code) # Decode if URL-encoded
248
+ log("Received authorization code: {}".format(auth_code))
249
+ if is_valid_authorization_code(auth_code):
250
+ try:
251
+ token_response = exchange_code_for_token(auth_code)
252
+ if 'access_token' not in token_response:
253
+ # Check for specific error message
254
+ if token_response.get("status") == "error":
255
+ self.send_response(400)
256
+ self.send_header('Content-type', 'text/html')
257
+ self.end_headers()
258
+ self.wfile.write(token_response["message"].encode())
259
+ return
260
+ # Handle other cases
261
+ raise ValueError("Access token not found in response.")
262
+ except Exception as e:
263
+ log("Error during token exchange: {}".format(e))
264
+ self.send_response(500)
265
+ self.send_header('Content-type', 'text/html')
266
+ self.end_headers()
267
+ self.wfile.write("An error occurred during authentication. Please try again.".encode())
268
+ else:
269
+ log("Token response: {}".format(token_response)) # Add this line
270
+ if 'access_token' in token_response:
271
+ with open(TOKEN_PATH, 'w') as token_file:
272
+ json.dump(token_response, token_file)
273
+ self.send_response(200)
274
+ self.send_header('Content-type', 'text/html')
275
+ self.end_headers()
276
+ self.wfile.write("Authentication successful. You can close this window now.".encode())
277
+ initiate_link_retrieval(config) # Pass config here
278
+ else:
279
+ log("Authentication failed with response: {}".format(token_response)) # Log the full response
280
+ if 'error' in token_response:
281
+ error_description = token_response.get('error_description', 'No description provided.')
282
+ log("Error details: {}".format(error_description)) # Log specific error details
283
+
284
+ # Provide user feedback based on the error
285
+ if token_response.get('error') == 'invalid_grant':
286
+ log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
287
+ check_invalid_grant_causes(auth_code)
288
+ clear_token_cache() # Clear the cache on invalid grant
289
+ user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
290
+ else:
291
+ user_message = "Authentication failed. Please check the logs for more details."
292
+
293
+ self.send_response(400)
294
+ self.send_header('Content-type', 'text/html')
295
+ self.end_headers()
296
+ self.wfile.write(user_message.encode())
297
+ shutdown_event.set() # Signal shutdown event after failed authentication
298
+ else:
299
+ log("Invalid authorization code format: {}".format(auth_code))
300
+ self.send_response(400)
301
+ self.send_header('Content-type', 'text/html')
302
+ self.end_headers()
303
+ self.wfile.write("Invalid authorization code format. Please try again.".encode())
304
+ shutdown_event.set() # Signal shutdown event after failed authentication
305
+ elif self.path == '/downloaded-emails':
306
+ self.send_response(200)
307
+ self._set_headers()
308
+ self.end_headers()
309
+ downloaded_emails = load_downloaded_emails()
310
+ response = json.dumps({"downloadedEmails": list(downloaded_emails)})
311
+ self.wfile.write(response.encode('utf-8'))
312
+ else:
313
+ self.send_response(200)
314
+ self.send_header('Access-Control-Allow-Origin', '*')
315
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
316
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
317
+ self.send_header('Content-type', 'text/html')
318
+ self.end_headers()
319
+ self.wfile.write(b'HTTPS server is running.')
320
+
321
+ def generate_self_signed_cert(cert_file, key_file):
322
+ log("Checking if certificate file exists: " + cert_file)
323
+ log("Checking if key file exists: " + key_file)
324
+ if not os.path.exists(cert_file) or not os.path.exists(key_file):
325
+ log("Generating self-signed SSL certificate...")
326
+ cmd = [
327
+ 'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
328
+ '-keyout', key_file,
329
+ '-out', cert_file,
330
+ '-days', '365'
331
+ #'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
332
+ ]
333
+ try:
334
+ log("Running command: " + ' '.join(cmd))
335
+ result = subprocess.call(cmd)
336
+ log("Command finished with result: " + str(result))
337
+ if result != 0:
338
+ raise RuntimeError("Failed to generate self-signed certificate")
339
+ log("Self-signed SSL certificate generated.")
340
+ except Exception as e:
341
+ log("Error generating self-signed certificate: {}".format(e))
342
+ raise
343
+
344
+ def run_server():
345
+ global httpd
346
+ try:
347
+ log("Attempting to start server on port " + str(server_port))
348
+ server_address = ('0.0.0.0', server_port) # Bind to all interfaces
349
+ httpd = HTTPServer(server_address, RequestHandler)
350
+ log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
351
+
352
+ if not os.path.exists(cert_file):
353
+ log("Error: Certificate file not found: " + cert_file)
354
+ if not os.path.exists(key_file):
355
+ log("Error: Key file not found: " + key_file)
356
+
357
+ httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
358
+ log("Starting HTTPS server on port {}".format(server_port))
359
+ httpd.serve_forever()
360
+ except Exception as e:
361
+ log("Error in serving: {}".format(e))
362
+ stop_server()
363
+
364
+ def stop_server():
365
+ global httpd
366
+ if httpd:
367
+ log("Stopping HTTPS server.")
368
+ httpd.shutdown()
369
+ httpd.server_close()
370
+ log("HTTPS server stopped.")
371
+ shutdown_event.set() # Signal shutdown event
372
+ bring_window_to_foreground() # Bring window to foreground after shutdown
373
+
374
+ def load_downloaded_emails():
375
+ downloaded_emails = set()
376
+ if os.path.exists(downloaded_emails_file):
377
+ with open(downloaded_emails_file, 'r') as file:
378
+ downloaded_emails = set(line.strip() for line in file)
379
+ log("Loaded downloaded emails: {}".format(downloaded_emails))
380
+ return downloaded_emails
381
+
382
+ def download_docx_files(links):
383
+ # Load the set of downloaded emails
384
+ # TODO Test if any of these have a .csv extension and then move those to the right location locally.
385
+ downloaded_emails = load_downloaded_emails()
386
+
387
+ for link in links:
388
+ try:
389
+ url = link.get('url', '')
390
+ filename = link.get('filename', '')
391
+
392
+ # Log the variables to debug
393
+ log("Processing link: url='{}', filename='{}'".format(url, filename))
394
+
395
+ # Skip if email already downloaded
396
+ if filename in downloaded_emails:
397
+ log("Skipping already downloaded email: {}".format(filename))
398
+ continue
399
+
400
+ log("Downloading .docx file from URL: {}".format(url))
401
+ response = requests.get(url, verify=False) # Set verify to False for self-signed certs
402
+ if response.status_code == 200:
403
+ file_path = os.path.join(local_storage_path, filename)
404
+ with open(file_path, 'wb') as file:
405
+ file.write(response.content)
406
+ log("Downloaded .docx file: {}".format(filename))
407
+ # Add to the set and save the updated list
408
+ downloaded_emails.add(filename)
409
+ with open(downloaded_emails_file, 'a') as file:
410
+ file.write(filename + '\n')
411
+ else:
412
+ log("Failed to download .docx file from URL: {}. Status code: {}".format(url, response.status_code))
413
+ except Exception as e:
414
+ log("Error downloading .docx file from URL: {}. Error: {}".format(url, e))
415
+
416
+ def open_browser_with_executable(url, browser_path=None):
417
+ try:
418
+ if browser_path:
419
+ log("Attempting to open URL with provided executable: {} {}".format(browser_path, url))
420
+ process = subprocess.Popen([browser_path, url], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
421
+ stdout, stderr = process.communicate()
422
+ if process.returncode == 0:
423
+ log("Browser opened with provided executable path using subprocess.Popen.")
424
+ else:
425
+ log("Browser failed to open using subprocess.Popen. Return code: {}. Stderr: {}".format(process.returncode, stderr))
426
+ else:
427
+ log("No browser path provided. Attempting to open URL with default browser: {}".format(url))
428
+ webbrowser.open(url)
429
+ log("Default browser opened.")
430
+ except Exception as e:
431
+ log("Failed to open browser: {}".format(e))
432
+
433
+ def initiate_link_retrieval(config):
434
+ log("Initiating browser via implicit GET.")
435
+ url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
436
+ open_browser_with_executable(url_get)
437
+
438
+ log("Preparing POST call.")
439
+ url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id']) # Use config here
440
+ downloaded_emails = list(load_downloaded_emails())
441
+ payload = {
442
+ "downloadedEmails": downloaded_emails
443
+ }
444
+
445
+ access_token = get_access_token()
446
+ if not access_token:
447
+ log("Access token not found. Please authenticate first.")
448
+ shutdown_event.set() # Signal shutdown event if token is not found
449
+ return
450
+
451
+ # Inspect the token to check its validity and permissions
452
+ token_info = inspect_token(access_token)
453
+ if token_info is None:
454
+ log("Access token is invalid. Please re-authenticate.")
455
+ shutdown_event.set() # Signal shutdown event if token is invalid
456
+ return
457
+
458
+ # Proceed with the rest of the function if the token is valid
459
+ headers = {
460
+ 'Authorization': 'Bearer {}'.format(access_token),
461
+ 'Content-Type': 'application/json'
462
+ }
463
+
464
+ log("Request headers: {}".format(headers))
465
+ log("Request payload: {}".format(payload))
466
+
467
+ handle_post_response(url, payload, headers)
468
+
469
+ def handle_post_response(url, payload, headers):
470
+ try:
471
+ response = requests.post(url, json=payload, headers=headers)
472
+ log("Response status code: {}".format(response.status_code))
473
+ log("Response body: {}".format(response.text))
474
+
475
+ if response.status_code == 200:
476
+ response_data = response.json()
477
+ log("Parsed response data: {}".format(response_data)) # Log the parsed response data
478
+ if response_data.get("status") == "error":
479
+ log("Error message from server: {}".format(response_data.get("message")))
480
+ print("Error: {}".format(response_data.get("message")))
481
+ shutdown_event.set() # Signal shutdown event after error
482
+ else:
483
+ log("Link retrieval initiated successfully.")
484
+ elif response.status_code == 401:
485
+ log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
486
+ # Inspect the token to log its details
487
+ token_info = inspect_token(headers['Authorization'].split(' ')[1])
488
+ log("Token details: {}".format(token_info))
489
+ shutdown_event.set()
490
+ elif response.status_code == 403:
491
+ log("Forbidden access. Ensure that the OAuth client has the correct permissions. Response body: {}".format(response.text))
492
+ shutdown_event.set()
493
+ elif response.status_code == 404:
494
+ log("Not Found. Verify the URL and ensure the Apps Script is deployed correctly. Response body: {}".format(response.text))
495
+ shutdown_event.set()
496
+ else:
497
+ log("Failed to initiate link retrieval. Unexpected status code: {}. Response body: {}".format(response.status_code, response.text))
498
+ shutdown_event.set()
499
+ except requests.exceptions.RequestException as e:
500
+ log("RequestException during link retrieval initiation: {}".format(e))
501
+ shutdown_event.set()
502
+ except Exception as e:
503
+ log("Unexpected error during link retrieval initiation: {}".format(e))
504
+ shutdown_event.set()
505
+
506
+ def inspect_token(access_token):
507
+ info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
508
+ try:
509
+ response = requests.get(info_url)
510
+ log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
511
+
512
+ if response.status_code == 200:
513
+ return response.json()
514
+ else:
515
+ log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
516
+ # Check for invalid token
517
+ if response.status_code == 400 and "invalid_token" in response.text:
518
+ log("Access token is invalid. Deleting token.json and stopping the server.")
519
+ delete_token_file() # Delete the token.json file
520
+ print("Access token is invalid. Please re-authenticate and restart the server.")
521
+ stop_server() # Stop the server
522
+ return None # Return None for invalid tokens
523
+ return None # Return None for other invalid tokens
524
+ except Exception as e:
525
+ log("Exception during token inspection: {}".format(e))
526
+ return None
527
+
528
+ def delete_token_file():
529
+ try:
530
+ if os.path.exists(TOKEN_PATH):
531
+ os.remove(TOKEN_PATH)
532
+ log("Deleted token.json successfully.")
533
+ else:
534
+ log("token.json does not exist.")
535
+ except Exception as e:
536
+ log("Error deleting token.json: {}".format(e))
537
+
538
+ def signal_handler(sig, frame):
539
+ log("Signal received: {}. Initiating shutdown.".format(sig))
540
+ stop_server()
541
+ sys.exit(0)
542
+
543
+ def auth_and_retrieval():
544
+ access_token = get_access_token()
545
+ if not access_token:
546
+ log("Access token not found or expired. Please authenticate first.")
547
+ #print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
548
+ auth_url = get_authorization_url()
549
+ #print(auth_url)
550
+ open_browser_with_executable(auth_url)
551
+ shutdown_event.wait() # Wait for the shutdown event to be set after authentication
552
+ else:
553
+ log("Access token found. Proceeding.")
554
+ initiate_link_retrieval(config) # Pass config here
555
+ shutdown_event.wait() # Wait for the shutdown event to be set
556
+
557
+ def is_valid_authorization_code(auth_code):
558
+ # Check if the authorization code is not None and is a non-empty string
559
+ if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
560
+ return True
561
+ log("Invalid authorization code format: {}".format(auth_code))
562
+ return False
563
+
564
+ def clear_token_cache():
565
+ if os.path.exists(TOKEN_PATH):
566
+ os.remove(TOKEN_PATH)
567
+ log("Cleared token cache.")
568
+
569
+ def check_invalid_grant_causes(auth_code):
570
+ # TODO Implement this function in the future to check for common causes of invalid_grant error
571
+ # Log potential causes for invalid_grant
572
+ log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
573
+ # Example checks (you can expand this based on your needs)
574
+ """
575
+ if is_code_used(auth_code):
576
+ log("Authorization code has already been used.")
577
+ if not is_redirect_uri_correct():
578
+ log("Redirect URI does not match the registered URI.")
579
+ """
580
+
581
+ if __name__ == "__main__":
582
+ signal.signal(signal.SIGINT, signal_handler)
583
+ signal.signal(signal.SIGTERM, signal_handler)
584
+
585
+ try:
586
+ # Generate SSL certificate if it doesn't exist
587
+ generate_self_signed_cert(cert_file, key_file)
588
+
589
+ from threading import Thread
590
+ log("Starting server thread.")
591
+ server_thread = Thread(target=run_server)
592
+ server_thread.daemon = True
593
+ server_thread.start()
594
+
595
+ auth_and_retrieval()
596
+
597
+ log("Stopping HTTPS server.")
598
+ stop_server() # Ensure the server is stopped
599
+ log("Waiting for server thread to finish.")
600
+ server_thread.join() # Wait for the server thread to finish
601
+ except KeyboardInterrupt:
602
+ log("KeyboardInterrupt received, stopping server.")
603
+ stop_server()
604
+ sys.exit(0)
605
+ except Exception as e:
606
+ log("An error occurred: {}".format(e))
607
+ stop_server()
581
608
  sys.exit(1)