Skip to content

Commit fa481f0

Browse files
authored
fix: dismiss small IBKR gateway dialogs (#71)
1 parent 8c3c9a3 commit fa481f0

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

2fa_bot.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@
3737
"no",
3838
"off",
3939
}
40+
DISMISS_SMALL_GATEWAY_DIALOGS = os.environ.get(
41+
"IBKR_DISMISS_SMALL_GATEWAY_DIALOGS",
42+
"yes",
43+
).strip().lower() not in {
44+
"0",
45+
"false",
46+
"no",
47+
"off",
48+
}
4049

4150
# Window titles to search for 2FA prompts. Live IBKR accounts can show mobile
4251
# push / IB Key wording instead of the shorter TOTP-oriented prompts.
@@ -71,10 +80,14 @@
7180
)
7281
DISMISSIBLE_DIALOG_SEARCH_PATTERNS = [
7382
"Login Messages",
83+
"IBKR Gateway",
7484
]
7585
DISMISSIBLE_DIALOG_TITLE_KEYWORDS = (
7686
"login messages",
7787
)
88+
SMALL_GATEWAY_DIALOG_TITLE = "ibkr gateway"
89+
SMALL_GATEWAY_DIALOG_MAX_WIDTH = 650
90+
SMALL_GATEWAY_DIALOG_MAX_HEIGHT = 220
7891
# Current IBKR Gateway TOTP prompts place the code field in the upper half of
7992
# the compact dialog. Keep the click centered on the text field instead of the
8093
# button/link area below it.
@@ -205,9 +218,21 @@ def is_auth_candidate(title):
205218
return any(keyword in normalized_title for keyword in AUTH_TITLE_KEYWORDS)
206219

207220

208-
def is_dismissible_dialog_candidate(title):
221+
def is_small_gateway_dialog(title, width, height):
222+
if not DISMISS_SMALL_GATEWAY_DIALOGS:
223+
return False
224+
if title.lower() != SMALL_GATEWAY_DIALOG_TITLE:
225+
return False
226+
if not width or not height:
227+
return False
228+
return width <= SMALL_GATEWAY_DIALOG_MAX_WIDTH and height <= SMALL_GATEWAY_DIALOG_MAX_HEIGHT
229+
230+
231+
def is_dismissible_dialog_candidate(title, width=None, height=None):
209232
normalized_title = title.lower()
210-
return any(keyword in normalized_title for keyword in DISMISSIBLE_DIALOG_TITLE_KEYWORDS)
233+
if any(keyword in normalized_title for keyword in DISMISSIBLE_DIALOG_TITLE_KEYWORDS):
234+
return True
235+
return is_small_gateway_dialog(title, width, height)
211236

212237

213238
def find_windows_by_patterns(patterns):
@@ -243,9 +268,9 @@ def find_dismissible_dialogs():
243268
candidates = []
244269
for window_id in reversed(find_windows_by_patterns(DISMISSIBLE_DIALOG_SEARCH_PATTERNS)):
245270
title = get_window_title(window_id)
246-
if not is_dismissible_dialog_candidate(title):
247-
continue
248271
width, height = get_window_geometry(window_id)
272+
if not is_dismissible_dialog_candidate(title, width, height):
273+
continue
249274
candidates.append(WindowCandidate(window_id, title, width, height))
250275
return candidates
251276

tests/test_docker_compose_ports.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,26 @@ grep -Fq ' - ACCEPT_API_FROM_IP=${ACCEPT_API_FROM_IP:?Set ACCEPT_API_FROM_I
5151

5252
grep -Fq 'INPUT_CLICK_POSITION = (0.50, 0.40)' "$repo_dir/2fa_bot.py"
5353
grep -Fq 'MIN_TOTP_SECONDS_REMAINING = 15' "$repo_dir/2fa_bot.py"
54+
grep -Fq 'SMALL_GATEWAY_DIALOG_MAX_WIDTH = 650' "$repo_dir/2fa_bot.py"
55+
grep -Fq 'SMALL_GATEWAY_DIALOG_MAX_HEIGHT = 220' "$repo_dir/2fa_bot.py"
56+
grep -Fq 'def is_small_gateway_dialog(title, width, height):' "$repo_dir/2fa_bot.py"
57+
grep -Fq 'is_dismissible_dialog_candidate(title, width, height)' "$repo_dir/2fa_bot.py"
5458
grep -Fq 'def type_totp_into_active_window(code):' "$repo_dir/2fa_bot.py"
59+
60+
REPO_DIR="$repo_dir" python3 <<'PY'
61+
import importlib.util
62+
import os
63+
import sys
64+
import types
65+
66+
sys.modules["pyotp"] = types.SimpleNamespace()
67+
module_path = os.path.join(os.environ["REPO_DIR"], "2fa_bot.py")
68+
spec = importlib.util.spec_from_file_location("ibkr_2fa_bot_test", module_path)
69+
module = importlib.util.module_from_spec(spec)
70+
spec.loader.exec_module(module)
71+
72+
assert module.is_dismissible_dialog_candidate("Login Messages")
73+
assert module.is_dismissible_dialog_candidate("IBKR Gateway", 509, 131)
74+
assert not module.is_dismissible_dialog_candidate("IBKR Gateway", 700, 550)
75+
assert not module.is_dismissible_dialog_candidate("IBKR Gateway", 790, 610)
76+
PY

0 commit comments

Comments
 (0)