optimuslib 0.0.45__tar.gz → 0.0.47__tar.gz
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.
- {optimuslib-0.0.45 → optimuslib-0.0.47}/PKG-INFO +5 -3
- optimuslib-0.0.47/README.md +1 -0
- {optimuslib-0.0.45 → optimuslib-0.0.47}/optimuslib/optimuslib.py +304 -51
- {optimuslib-0.0.45 → optimuslib-0.0.47}/pyproject.toml +1 -1
- optimuslib-0.0.45/README.md +0 -0
- {optimuslib-0.0.45 → optimuslib-0.0.47}/optimuslib/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: optimuslib
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.47
|
|
4
4
|
Summary: Function Library for mostly used codes
|
|
5
5
|
Author: Shomi Nanwani
|
|
6
6
|
Requires-Python: >=3.9,<4.0
|
|
@@ -9,10 +9,12 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
14
|
Requires-Dist: pillow (==9.5.0)
|
|
13
15
|
Requires-Dist: pyperclip (>=1.8.2,<2.0.0)
|
|
14
16
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
15
17
|
Requires-Dist: requests-futures (>=1.0.0,<2.0.0)
|
|
16
18
|
Description-Content-Type: text/markdown
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Optimuslib
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Optimuslib
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
#########################################################
|
|
25
25
|
# Local testing on
|
|
26
26
|
# C:\Users\Admin\AppData\Local\Programs\Python\Python311\Lib\site-packages\optimuslib
|
|
27
|
-
# /home/pi/.local/lib/python3.
|
|
27
|
+
# /home/pi/.local/lib/python3.11/site-packages/optimuslib/optimuslib.py
|
|
28
|
+
# ~/.local/lib/python3.11/site-packages/optimuslib/optimuslib.py
|
|
28
29
|
|
|
29
30
|
### import code
|
|
30
31
|
# import optimuslib
|
|
@@ -93,19 +94,6 @@
|
|
|
93
94
|
# python -m pip install --no-cache-dir --upgrade -r requirements.txt
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
###### CHECK OS AND PERFORM COMMAND ACCORDINGLY
|
|
97
|
-
# get OS version
|
|
98
|
-
import platform
|
|
99
|
-
def checkOS():
|
|
100
|
-
osType = platform.system()
|
|
101
|
-
osVersion = platform.platform()
|
|
102
|
-
oshostname = platform.node()
|
|
103
|
-
# log.info('osType: %s, osVersion: %s, oshostname: %s', osType, osVersion, oshostname)
|
|
104
|
-
print(f'osType: {osType}, osVersion: {osVersion}, oshostname: {oshostname}')
|
|
105
|
-
return osType, osVersion, oshostname
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
97
|
|
|
110
98
|
############## LOG INFO IN TERMINAL AND log.txt FILE ###################
|
|
111
99
|
import logging
|
|
@@ -144,6 +132,17 @@ def loglib(data):
|
|
|
144
132
|
log.info('%d %s' % ('1', 'a'))
|
|
145
133
|
log.info('%d %s' % '1' % 'a')
|
|
146
134
|
|
|
135
|
+
###### CHECK OS AND PERFORM COMMAND ACCORDINGLY
|
|
136
|
+
# get OS version
|
|
137
|
+
import platform
|
|
138
|
+
def checkOS():
|
|
139
|
+
osType = platform.system()
|
|
140
|
+
osVersion = platform.platform()
|
|
141
|
+
oshostname = platform.node()
|
|
142
|
+
# log.info('osType: %s, osVersion: %s, oshostname: %s', osType, osVersion, oshostname)
|
|
143
|
+
log.info(f'osType: {osType}, osVersion: {osVersion}, oshostname: {oshostname}')
|
|
144
|
+
return osType, osVersion, oshostname
|
|
145
|
+
|
|
147
146
|
|
|
148
147
|
osType, osVersion, oshostname = checkOS()
|
|
149
148
|
if osType == 'Windows':
|
|
@@ -251,6 +250,7 @@ def traverse(path):
|
|
|
251
250
|
|
|
252
251
|
# '''
|
|
253
252
|
############## REQUESTS LIBRARY ###################
|
|
253
|
+
'''
|
|
254
254
|
import time, sys, requests
|
|
255
255
|
# import sys
|
|
256
256
|
import requests
|
|
@@ -335,11 +335,11 @@ def sendRequest(method, requestUrl, cookies=None, headers=None, data=None, json=
|
|
|
335
335
|
s.proxies = { 'http': proxyhost, 'https': proxyhost}
|
|
336
336
|
# pythonproxy.parseProxyHostAndRun(proxyhost)
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
if not validResponseCodes:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
'''
|
|
339
|
+
# if not validResponseCodes:
|
|
340
|
+
# # validResponseCodes = [200,201,302,307,401,400,403,404,405,406]
|
|
341
|
+
# validResponseCodes = [200,201,302,307,401,400,404,405,406]
|
|
342
|
+
'''
|
|
343
343
|
totalRequestsSent += 1
|
|
344
344
|
|
|
345
345
|
# log.info('[%d/%d] Sending Request: %s',totalRequestsSent,len(requestUrl),requestUrl)
|
|
@@ -365,8 +365,8 @@ def sendRequest(method, requestUrl, cookies=None, headers=None, data=None, json=
|
|
|
365
365
|
# except (MalformedRequest, InternalError, StatusUnknown, ConnectionError, ConnectionResetError, http.client.RemoteDisconnected, RemoteDisconnected, ProtocolError, HTTPException, socket.gaierror) as e:
|
|
366
366
|
except requests.exceptions.ConnectionError as e:
|
|
367
367
|
log.error(f'ConnectionError occured - {e}')
|
|
368
|
-
|
|
369
|
-
time.sleep(
|
|
368
|
+
log.error('Sleeping for 15 secods before next request')
|
|
369
|
+
time.sleep(15)
|
|
370
370
|
sendRequest(method, requestUrl, cookies, headers, data, json, params, files, timeout, proxyhost, validResponseCodes, allow_redirects, verify)
|
|
371
371
|
# sys.exit()
|
|
372
372
|
except requests.exceptions.ConnectionResetError as e:
|
|
@@ -374,7 +374,7 @@ def sendRequest(method, requestUrl, cookies=None, headers=None, data=None, json=
|
|
|
374
374
|
log.error('Is your internet connection Stable?')
|
|
375
375
|
sys.exit()
|
|
376
376
|
except requests.exceptions.RequestException as e:
|
|
377
|
-
|
|
377
|
+
log.error(f"Request failed: {e}")
|
|
378
378
|
except ConnectionRefusedError as e:
|
|
379
379
|
log.error(f'Exception occured - {e}')
|
|
380
380
|
log.error('Is proxy set and server not running?')
|
|
@@ -419,6 +419,122 @@ def sendBulkRequests(method, requestUrlList, cookies=None, headers=None, data=No
|
|
|
419
419
|
|
|
420
420
|
################# REQUESTS MODULE DEBUGGING
|
|
421
421
|
'''
|
|
422
|
+
########## CORRECTED BY CHATGPT
|
|
423
|
+
import time, sys, requests, logging
|
|
424
|
+
from requests.adapters import HTTPAdapter, Retry
|
|
425
|
+
from requests_futures.sessions import FuturesSession
|
|
426
|
+
requests.packages.urllib3.disable_warnings()
|
|
427
|
+
|
|
428
|
+
# Setup logging
|
|
429
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
430
|
+
log = logging.getLogger(__name__)
|
|
431
|
+
|
|
432
|
+
# Response codes to force retry on
|
|
433
|
+
RETRY_STATUS_CODES = [429, 500, 502, 503, 504]
|
|
434
|
+
|
|
435
|
+
# Max retry attempts
|
|
436
|
+
MAX_RETRIES = 5
|
|
437
|
+
|
|
438
|
+
# Futures Session for parallel async requests
|
|
439
|
+
futuresworkercount = 100
|
|
440
|
+
s = FuturesSession(max_workers=futuresworkercount)
|
|
441
|
+
|
|
442
|
+
# Configure retry strategy
|
|
443
|
+
retry_strategy = Retry(
|
|
444
|
+
total=MAX_RETRIES,
|
|
445
|
+
backoff_factor=1, # Exponential backoff: 1,2,4,8...
|
|
446
|
+
status_forcelist=RETRY_STATUS_CODES,
|
|
447
|
+
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"],
|
|
448
|
+
raise_on_status=False
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
452
|
+
s.mount("https://", adapter)
|
|
453
|
+
s.mount("http://", adapter)
|
|
454
|
+
|
|
455
|
+
totalRequestsSent = 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _handle_429(response, attempt):
|
|
459
|
+
"""Handles 429 Too Many Requests with delay/retry."""
|
|
460
|
+
if response.status_code != 429:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
retry_after = response.headers.get("Retry-After")
|
|
464
|
+
if retry_after:
|
|
465
|
+
delay = int(retry_after)
|
|
466
|
+
else:
|
|
467
|
+
delay = min(2 ** attempt, 60) # exponential backoff, max 60s
|
|
468
|
+
|
|
469
|
+
log.warning(f"429 Too Many Requests - sleeping {delay}s before retry...")
|
|
470
|
+
time.sleep(delay)
|
|
471
|
+
return True
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def sendRequest(method, requestUrl, cookies=None, headers=None, data=None, json=None, params=None, files=None, timeout=None, proxyhost=None, allow_redirects=True, verify=False):
|
|
475
|
+
"""Send HTTP request with retry + 429 handling."""
|
|
476
|
+
global totalRequestsSent
|
|
477
|
+
totalRequestsSent += 1
|
|
478
|
+
|
|
479
|
+
if proxyhost:
|
|
480
|
+
log.debug('Using proxy: %s', proxyhost)
|
|
481
|
+
s.proxies = {'http': proxyhost, 'https': proxyhost}
|
|
482
|
+
|
|
483
|
+
log.debug(f'[{totalRequestsSent}] Sending Request: {requestUrl}')
|
|
484
|
+
|
|
485
|
+
method = method.lower()
|
|
486
|
+
attempts = 0
|
|
487
|
+
|
|
488
|
+
while attempts < MAX_RETRIES:
|
|
489
|
+
try:
|
|
490
|
+
if method == "get":
|
|
491
|
+
response = s.get(requestUrl, cookies=cookies, headers=headers, params=params, timeout=timeout, allow_redirects=allow_redirects, verify=verify)
|
|
492
|
+
elif method == "post":
|
|
493
|
+
response = s.post(requestUrl, cookies=cookies, headers=headers, data=data, json=json, files=files, params=params, timeout=timeout, allow_redirects=allow_redirects, verify=verify)
|
|
494
|
+
elif method == "patch":
|
|
495
|
+
response = s.patch(requestUrl, cookies=cookies, headers=headers, data=data, json=json, files=files, params=params, timeout=timeout, allow_redirects=allow_redirects, verify=verify)
|
|
496
|
+
elif method == "delete":
|
|
497
|
+
response = s.delete(requestUrl, cookies=cookies, headers=headers, data=data, json=json, files=files, params=params, timeout=timeout, allow_redirects=allow_redirects, verify=verify)
|
|
498
|
+
elif method == "put":
|
|
499
|
+
response = s.put(requestUrl, cookies=cookies, headers=headers, data=data, json=json, files=files, params=params, timeout=timeout, allow_redirects=allow_redirects, verify=verify)
|
|
500
|
+
else:
|
|
501
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
502
|
+
|
|
503
|
+
result = response.result()
|
|
504
|
+
|
|
505
|
+
# Handle 429 manually
|
|
506
|
+
if result.status_code == 429:
|
|
507
|
+
if _handle_429(result, attempts):
|
|
508
|
+
attempts += 1
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
log.debug(f"Response [{result.status_code}] - {result.text}...")
|
|
512
|
+
# log.debug(f"Response [{result.status_code}] - {result.text[:200]}...")
|
|
513
|
+
return response
|
|
514
|
+
|
|
515
|
+
except requests.exceptions.ConnectionError as e:
|
|
516
|
+
log.error(f'ConnectionError: {e} - retrying in 5s...')
|
|
517
|
+
time.sleep(5)
|
|
518
|
+
except requests.exceptions.RequestException as e:
|
|
519
|
+
log.error(f"Request failed: {e}")
|
|
520
|
+
break
|
|
521
|
+
|
|
522
|
+
attempts += 1
|
|
523
|
+
|
|
524
|
+
log.error(f"Request failed after {MAX_RETRIES} attempts: {requestUrl}")
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def sendBulkRequests(method, requestUrlList, **kwargs):
|
|
529
|
+
"""Send multiple requests in bulk."""
|
|
530
|
+
futureResponseList = []
|
|
531
|
+
for idx, requestUrl in enumerate(requestUrlList, 1):
|
|
532
|
+
log.debug(f'Sending Bulk Request...[{idx}/{len(requestUrlList)}]')
|
|
533
|
+
futureResponse = sendRequest(method, requestUrl, **kwargs)
|
|
534
|
+
futureResponseList.append(futureResponse)
|
|
535
|
+
return futureResponseList
|
|
536
|
+
########### CORRECTED BY CHATGPT
|
|
537
|
+
'''
|
|
422
538
|
# GET ALL COOKIES IN SESSION
|
|
423
539
|
print(s.cookies.get_dict())
|
|
424
540
|
|
|
@@ -922,7 +1038,7 @@ def select_file_from_computer(image_path):
|
|
|
922
1038
|
autoit.win_active("Open")
|
|
923
1039
|
autoit.control_set_text("Open", "Edit1", image_path)
|
|
924
1040
|
autoit.control_send("Open", "Edit1", "{ENTER}")
|
|
925
|
-
log.info("Sleeping for
|
|
1041
|
+
log.info(f"Sleeping for {wait_time} seconds...", )
|
|
926
1042
|
sleep(wait_time)
|
|
927
1043
|
|
|
928
1044
|
# click_function('Sign In',"//a[@data-nav-ref='nav_ya_signin']",)
|
|
@@ -932,11 +1048,13 @@ def select_file_from_computer(image_path):
|
|
|
932
1048
|
############################
|
|
933
1049
|
import os
|
|
934
1050
|
def createFolder(foldername):
|
|
935
|
-
try:
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
except FileExistsError:
|
|
939
|
-
|
|
1051
|
+
# try:
|
|
1052
|
+
# if os.mkdir(foldername):
|
|
1053
|
+
# log.info(f"Folder Created: {foldername}")
|
|
1054
|
+
# except FileExistsError:
|
|
1055
|
+
# log.info(f"{foldername} folder already exists")
|
|
1056
|
+
if not os.path.exists(foldername):
|
|
1057
|
+
os.makedirs(foldername)
|
|
940
1058
|
|
|
941
1059
|
############################
|
|
942
1060
|
import re
|
|
@@ -1140,13 +1258,13 @@ def textfile_to_image(text_file_path, image_output_path):
|
|
|
1140
1258
|
for font_filename in COMMON_MONO_FONT_FILENAMES:
|
|
1141
1259
|
try:
|
|
1142
1260
|
font = ImageFont.truetype(font_filename, size=large_font)
|
|
1143
|
-
|
|
1261
|
+
log.error(f'Using font "{font_filename}".')
|
|
1144
1262
|
break
|
|
1145
1263
|
except IOError:
|
|
1146
|
-
|
|
1264
|
+
log.error(f'Could not load font "{font_filename}".')
|
|
1147
1265
|
if font is None:
|
|
1148
1266
|
font = ImageFont.load_default()
|
|
1149
|
-
|
|
1267
|
+
log.error('Using default font.')
|
|
1150
1268
|
|
|
1151
1269
|
# make a sufficiently sized background image based on the combination of font and lines
|
|
1152
1270
|
font_points_to_pixels = lambda pt: round(pt * 96.0 / 72)
|
|
@@ -1206,28 +1324,34 @@ def logAndSend(botToken,chatId,output):
|
|
|
1206
1324
|
sendDiscordBotNotification(botToken,chatId,output)
|
|
1207
1325
|
|
|
1208
1326
|
########## format a number as per indian currency
|
|
1209
|
-
def
|
|
1210
|
-
|
|
1327
|
+
def formatCurrencyIndian(number):
|
|
1328
|
+
"""
|
|
1329
|
+
Format a number as per Indian currency format with commas.
|
|
1330
|
+
Example: 12345678.90 -> '1,23,45,678.90'
|
|
1331
|
+
"""
|
|
1211
1332
|
number_str = "{:.2f}".format(number)
|
|
1212
1333
|
integer_part, decimal_part = number_str.split(".")
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1334
|
+
# Handle negative numbers
|
|
1335
|
+
sign = ""
|
|
1336
|
+
if integer_part.startswith('-'):
|
|
1337
|
+
sign = "-"
|
|
1338
|
+
integer_part = integer_part[1:]
|
|
1339
|
+
|
|
1340
|
+
# First group (last 3 digits)
|
|
1341
|
+
if len(integer_part) > 3:
|
|
1342
|
+
last3 = integer_part[-3:]
|
|
1343
|
+
rest = integer_part[:-3]
|
|
1344
|
+
# Group rest in pairs
|
|
1345
|
+
pairs = []
|
|
1346
|
+
while len(rest) > 2:
|
|
1347
|
+
pairs.insert(0, rest[-2:])
|
|
1348
|
+
rest = rest[:-2]
|
|
1349
|
+
if rest:
|
|
1350
|
+
pairs.insert(0, rest)
|
|
1351
|
+
formatted = sign + ",".join(pairs + [last3]) + "." + decimal_part
|
|
1352
|
+
else:
|
|
1353
|
+
formatted = sign + integer_part + "." + decimal_part
|
|
1354
|
+
return formatted
|
|
1231
1355
|
|
|
1232
1356
|
########### DISCORD BOT NOTIFICATION - BOT DETAILS ##############
|
|
1233
1357
|
# def getDiscordBotInfo(botToken):
|
|
@@ -1325,6 +1449,9 @@ def fetchOTPDiscord(botToken, otpFetchWaitTime, otpChannelId):
|
|
|
1325
1449
|
id, timestamp, username, content = getDiscordBotUpdates(botToken, otpChannelId, messageCount=1)
|
|
1326
1450
|
log.info('update_id: %s, text: %s', id, content)
|
|
1327
1451
|
twofa_value = content[-6:]
|
|
1452
|
+
twofa_value = twofa_value.replace("-","")
|
|
1453
|
+
twofa_value = twofa_value.replace("S","")
|
|
1454
|
+
twofa_value = twofa_value.replace("T","")
|
|
1328
1455
|
getVarInfo('twofa_value',twofa_value)
|
|
1329
1456
|
break
|
|
1330
1457
|
except ValueError as e:
|
|
@@ -1410,6 +1537,132 @@ def buildUnicodeTable(headers, rows):
|
|
|
1410
1537
|
# fetchOTPDiscord(botToken, otpFetchWaitTime, otpChannelId)
|
|
1411
1538
|
# sendDiscordBotNotification(botToken,channelId,message)
|
|
1412
1539
|
# sendDiscordBotFile(botToken,channelId,filePath)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
########################## FETCH OTP FROM GMAIL VIA APP PASSWORD ##########################
|
|
1543
|
+
import imaplib
|
|
1544
|
+
import email
|
|
1545
|
+
import time
|
|
1546
|
+
import re
|
|
1547
|
+
import quopri
|
|
1548
|
+
import threading
|
|
1549
|
+
from bs4 import BeautifulSoup
|
|
1550
|
+
from email.utils import parsedate_to_datetime
|
|
1551
|
+
from datetime import datetime, timezone
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
def extract_clean_text(msg):
|
|
1555
|
+
"""Extract readable text from HTML email"""
|
|
1556
|
+
text = ""
|
|
1557
|
+
for part in msg.walk():
|
|
1558
|
+
if part.get_content_type() == "text/html":
|
|
1559
|
+
raw = part.get_payload(decode=True)
|
|
1560
|
+
decoded = quopri.decodestring(raw).decode(errors="ignore")
|
|
1561
|
+
soup = BeautifulSoup(decoded, "html.parser")
|
|
1562
|
+
text += soup.get_text(" ")
|
|
1563
|
+
return text
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def is_recent(msg):
|
|
1567
|
+
"""Check if email was received within MAX_AGE_SECONDS"""
|
|
1568
|
+
date_hdr = msg.get("Date")
|
|
1569
|
+
if not date_hdr:
|
|
1570
|
+
return False
|
|
1571
|
+
|
|
1572
|
+
try:
|
|
1573
|
+
msg_time = parsedate_to_datetime(date_hdr)
|
|
1574
|
+
if msg_time.tzinfo is None:
|
|
1575
|
+
msg_time = msg_time.replace(tzinfo=timezone.utc)
|
|
1576
|
+
|
|
1577
|
+
now = datetime.now(timezone.utc)
|
|
1578
|
+
age = (now - msg_time).total_seconds()
|
|
1579
|
+
return 0 <= age <= MAX_AGE_SECONDS
|
|
1580
|
+
except Exception:
|
|
1581
|
+
return False
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
def input_with_timeout(prompt, timeout):
|
|
1585
|
+
"""Get user input with timeout"""
|
|
1586
|
+
result = {"value": None}
|
|
1587
|
+
|
|
1588
|
+
def ask():
|
|
1589
|
+
try:
|
|
1590
|
+
result["value"] = input(prompt)
|
|
1591
|
+
except EOFError:
|
|
1592
|
+
pass
|
|
1593
|
+
|
|
1594
|
+
t = threading.Thread(target=ask, daemon=True)
|
|
1595
|
+
t.start()
|
|
1596
|
+
t.join(timeout)
|
|
1597
|
+
|
|
1598
|
+
return result["value"]
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
# ============== IMAP OTP ==================
|
|
1602
|
+
|
|
1603
|
+
def poll_for_otp(EMAIL, APP_PASSWORD, SENDER, SUBJECT, POLL_INTERVAL, POLL_TIMEOUT, MAX_AGE_SECONDS):
|
|
1604
|
+
"""Poll Gmail IMAP for OTP email"""
|
|
1605
|
+
IMAP_SERVER = "imap.gmail.com"
|
|
1606
|
+
mail = imaplib.IMAP4_SSL(IMAP_SERVER)
|
|
1607
|
+
mail.login(EMAIL, APP_PASSWORD)
|
|
1608
|
+
|
|
1609
|
+
start = time.time()
|
|
1610
|
+
|
|
1611
|
+
while time.time() - start < POLL_TIMEOUT:
|
|
1612
|
+
# 🔑 Gmail IMAP requires re-select to refresh UNSEEN
|
|
1613
|
+
mail.select("inbox", readonly=False)
|
|
1614
|
+
|
|
1615
|
+
status, messages = mail.search(
|
|
1616
|
+
None,
|
|
1617
|
+
f'(UNSEEN FROM "{SENDER}" SUBJECT "{SUBJECT}")'
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
ids = messages[0].split()
|
|
1621
|
+
|
|
1622
|
+
for eid in ids:
|
|
1623
|
+
_, data = mail.fetch(eid, "(RFC822)")
|
|
1624
|
+
msg = email.message_from_bytes(data[0][1])
|
|
1625
|
+
|
|
1626
|
+
if not is_recent(msg):
|
|
1627
|
+
continue
|
|
1628
|
+
|
|
1629
|
+
body = extract_clean_text(msg)
|
|
1630
|
+
match = re.search(r"OTP[^0-9]*(\d{6})", body, re.IGNORECASE)
|
|
1631
|
+
|
|
1632
|
+
if match:
|
|
1633
|
+
# mark email as read
|
|
1634
|
+
mail.store(eid, "+FLAGS", "\\Seen")
|
|
1635
|
+
mail.logout()
|
|
1636
|
+
return match.group(1)
|
|
1637
|
+
|
|
1638
|
+
log.info("Waiting for OTP...")
|
|
1639
|
+
time.sleep(POLL_INTERVAL)
|
|
1640
|
+
|
|
1641
|
+
mail.logout()
|
|
1642
|
+
raise TimeoutError("Auto OTP fetch timed out")
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def get_gmail_otp(EMAIL, APP_PASSWORD, SENDER, SUBJECT,POLL_INTERVAL,POLL_TIMEOUT,MAX_AGE_SECONDS,MANUAL_INPUT_TIMEOUT):
|
|
1646
|
+
"""Get OTP automatically or fall back to manual input"""
|
|
1647
|
+
try:
|
|
1648
|
+
otp = poll_for_otp(EMAIL, APP_PASSWORD, SENDER, SUBJECT, POLL_INTERVAL, POLL_TIMEOUT, MAX_AGE_SECONDS)
|
|
1649
|
+
log.info("OTP received automatically:", otp)
|
|
1650
|
+
return otp
|
|
1651
|
+
|
|
1652
|
+
except TimeoutError:
|
|
1653
|
+
log.info("⚠️ Auto OTP fetch timed out.")
|
|
1654
|
+
log.info(f"Please enter OTP manually (waiting {MANUAL_INPUT_TIMEOUT} seconds)...")
|
|
1655
|
+
|
|
1656
|
+
manual_otp = input_with_timeout("Enter OTP: ", MANUAL_INPUT_TIMEOUT)
|
|
1657
|
+
|
|
1658
|
+
if manual_otp and manual_otp.strip().isdigit():
|
|
1659
|
+
return manual_otp.strip()
|
|
1660
|
+
|
|
1661
|
+
raise TimeoutError("No OTP entered within manual input window")
|
|
1662
|
+
|
|
1663
|
+
# ================== RUN ===================
|
|
1664
|
+
|
|
1665
|
+
|
|
1413
1666
|
################################################
|
|
1414
1667
|
log.info('[+] Optimuslib Import Sucessfull.')
|
|
1415
1668
|
|
optimuslib-0.0.45/README.md
DELETED
|
File without changes
|
|
File without changes
|